<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>민민의 하드디스크</title>
    <link>https://2minmin2.tistory.com/</link>
    <description>아무거나 넣는 하드디스크</description>
    <language>ko</language>
    <pubDate>Tue, 9 Jun 2026 09:11:41 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>민민2</managingEditor>
    <image>
      <title>민민의 하드디스크</title>
      <url>https://tistory1.daumcdn.net/tistory/6242022/attach/0e2a5863d5554b63bf037b05cbaf8bdb</url>
      <link>https://2minmin2.tistory.com</link>
    </image>
    <item>
      <title>[DB] 트리거와 시퀀스가 만든 데이터 이관 문제 해결 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/119</link>
      <description>&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실무에서 시스템을 운영하다 보면, 데이터의 변경 이력을 엄격하게 추적하기 위해 원본 테이블과 1:1로 매칭되는 이력 테이블(_hst)을 두는 경우가 매우 흔하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제를 겪었던 프로젝트는 원본 테이블에 CUD(Create, Update, Delete)가 발생하면 데이터베이스 트리거(Trigger)가 백그라운드에서 돌아가고, 시퀀스(Sequence)의 NEXTVAL을 호출해 이력 테이블의 PK(이력 일련번호)를 채우며 INSERT 되는 아키텍처를 띈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비즈니스 로직에 개입하지 않고 DB 단에서 조용하고 확실하게 이력을 남길 수 있어 자주 쓰이는 패턴이다. 그런데, 기존 시스템(AS-IS)에서 새로운 시스템(TO-BE)으로 대규모 데이터를 이관하는 과정에서 이 뻔한 구조가 거대한 폭탄으로 돌아오는 현상을 겪었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제의 발단: 이관 서버의 시퀀스 초기화&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사건의 발단은 데이터 이관 작업 후 발생했다. 기존 운영 DB에 쌓여있던 수백만 건의 원본 데이터와 이력(_hst) 데이터를 무사히 TO-BE DB로 마이그레이션하는 데 성공했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제는 '시퀀스 객체의 상태'였다. 테이블의 데이터는 있는 그대로 잘 퍼왔지만, TO-BE 환경에 시퀀스를 새로 생성(또는 초기화)하면서 시작값(START WITH)을 이관된 데이터의 MAX(PK) 값으로 맞춰주지 않고 기본값인 1로 시작해 버린 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기괴한 현상: 시간이 거꾸로 흐르는 이력 조회&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시스템을 오픈하고 사용자들이 데이터를 수정하기 시작하자, 이력 테이블의 조회가 기괴하게 꼬이기 시작했다. 분명 방금 수정한 최신 데이터인데, 이력 조회 화면에서는 보이지 않거나 맨 마지막 페이지 구석에 처박혀 있는 현상이 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원인을 파악하기 위해 로그를 까보니, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;문제는 &lt;b data-index-in-node=&quot;25&quot; data-path-to-node=&quot;11&quot;&gt;이력 테이블을 조회할 때 정렬하는 기준&lt;/b&gt;&lt;/span&gt;에 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;보통 특정 데이터의 변경 이력을 화면에 뿌려줄 때, 최신순으로 보여주기 위해 시간 데이터 대신 인덱스가 타기 쉬운 이&lt;span style=&quot;background-color: #f6e199;&quot;&gt;력 테이블의 PK(시퀀스 값)를 기준으로 내림차순 정렬(ORDER BY HST_SEQ DESC)&lt;/span&gt;을 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 시퀀스가 초기화된 상태에서 트리거가 동작하자 다음과 같은 타임라인 역전 현상이 벌어졌다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제 예시&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,0&quot;&gt;AS-IS 데이터 이관 직후 상태:&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 회원 '홍길동'의 이력 데이터 중 가장 마지막 시퀀스 번호: 100,500&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이관된 이력 테이블의 MAX(HST_SEQ): 100,500&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,0,1,2,0&quot;&gt;TO-BE DB의 시퀀스 현재 값: 1 (오류의 시작)&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,1,0&quot;&gt;TO-BE 시스템 오픈 후 CUD 발생:&lt;/b&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;15,1,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사용자가 '홍길동'의 전화번호를 수정 (Update)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;트리거 동작 &amp;rarr; _hst 테이블에 INSERT 발생&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이때 TO-BE의 시퀀스가 1부터 시작하므로, 방금 수정한 최신 이력의 PK로 1이 채번됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이력 조회 화면 쿼리 실행&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1772434072451&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM MEMBER_HST 
 WHERE USER_ID = '홍길동' 
 ORDER BY HST_SEQ DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; &lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;15,3,0&quot;&gt;조회 결과:&lt;/b&gt; 정렬 기준이 HST_SEQ DESC이므로, 과거 시스템에서 이관된 번호인 100,500번 데이터가 화면 맨 위에(가장 최신인 것처럼) 노출된다. 정작 방금 수정한 진짜 최신 데이터는 번호가 1이기 때문에 과거 이력의 맨 밑바닥으로 가라앉아 버렸다.&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;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 해결&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;만약 이력 테이블의 PK에 Unique Constraint(고유 제약조건)라도 걸려있었다면 PK 무결성 제약조건 위배(Duplicate Key) 에러가 나면서 CUD 자체가 롤백되어 바로 눈치챘을 것이다. 하지만 이력 테이블 특성상 제약조건을 느슨하게 풀어두는 경우가 많아 에러 없이 조용히 데이터가 꼬여가고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;결국 사태를 파악한 직후, 다음 두 가지 조치를 긴급하게 수행했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,0,0&quot;&gt;시퀀스 동기화:&lt;/b&gt; TO-BE DB의 이력 시퀀스를 삭제하고, 이관된 이력 데이터의 MAX(HST_SEQ) + 1 값으로 START WITH를 지정해 시퀀스를 재성성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;19,1,0&quot;&gt;꼬인 데이터 보정:&lt;/b&gt; 이미 1번부터 채번되어 잘못 들어가 버린 최신 이력 데이터들의 PK를 찾아내, 정상적인 시퀀스 대역으로 수동 UPDATE 처리를 진행&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 이관 시 테이블의 데이터를 옮기는 것에만 혈안이 되어, 그 데이터를 지탱하는 '시퀀스의 현재 값'을 동기화하는 것을 누락하면 시스템 전체의 정합성이 얼마나 우스워질 수 있는지 뼈저리게 느꼈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;느낀점&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특히 금융이나 공공 도메인처럼 데이터의 이력(Audit) 증명이 절대적으로 중요한 환경에서, 타임라인이 꼬이는 것은 단순한 버그 이상의 치명적인 결함이 될 수 있다고 생각했고,,,,&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;사실, 프로시저나 트리거는 사용하지 않는 것이 좋을 것 같다고 생각이 들었다. &lt;/span&gt;&lt;span&gt;디버깅도 쉽지 않고, 관리할 항목이 추가되고, 만약 api가 호출돼서 CUD 로직을 탈 때 트리거 오류가 나면 실행 자체가 되지 않는다. 필요하다면 사용해야겠지만 굳이굳이? 사용할 필요는 없는 것 같음.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>실무</category>
      <category>db설계</category>
      <category>데이터이관</category>
      <category>마이그레이션</category>
      <category>백엔드</category>
      <category>시퀀스</category>
      <category>실무경험</category>
      <category>오라클db</category>
      <category>장애회고</category>
      <category>차세대시스템</category>
      <category>트리거</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/119</guid>
      <comments>https://2minmin2.tistory.com/119#entry119comment</comments>
      <pubDate>Mon, 2 Mar 2026 16:27:57 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 대용량 페이징 조회 성능 개선 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/118</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;매일 쌓이는 데이터를 조회하는 화면에서 &lt;b&gt;수백만 건 이상 데이터 페이징 조회&lt;/b&gt;가 필요했다.&lt;br&gt;단순히 SELECT * (모든 컬럼 또는 많은 컬럼들)로 페이징을 처리하면 응답 시간이 급격히 느려졌고, DB 부하도 눈에 띄게 증가했다. (화면 조회조건으로 돌릴 시 대충 10 ~ 20초 이상 걸림), 조회조건에 PK를 사용한다기 보단 기간(날짜 from ~ to)과 공통코드들을 위주로 조회하기 때문에 인덱스가 안 걸려있음.&lt;br&gt;실무에서 효과가 있었던 방법은 &lt;b&gt;CTE(또는 서브쿼리)로 PK만 먼저 조회한 뒤, 그 결과를 다시 조인해서 상세 컬럼을 조회하는 방식&lt;/b&gt;이다.&lt;br&gt;이 글에서는 &lt;b&gt;왜 이 방식이 빠른지에 대한 원리&lt;/b&gt;와 &lt;b&gt;실제 SQL 예시&lt;/b&gt;를 정리한다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기존 방식의 문제점 (모든 컬럼을 바로 조회)&lt;/h2&gt;&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT  *
FROM    EMP_LOG
WHERE   USE_YN = 'Y'
ORDER BY REG_DT DESC
OFFSET  :offset ROWS FETCH NEXT :pageSize ROWS ONLY;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제점&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;테이블 건수: 100만 건 이상&lt;/li&gt;&lt;li&gt;컬럼 수가 많고 (VARCHAR, CLOB 포함)&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;DB 입장에서는&lt;/p&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;조건에 맞는 전체 ROW 탐색&lt;/li&gt;&lt;li&gt;정렬 수행&lt;/li&gt;&lt;li&gt;OFFSET 이전 ROW도 모두 처리&lt;/li&gt;&lt;li&gt;그 과정에서 &lt;b&gt;모든 컬럼을 메모리/디스크에서 읽음&lt;/b&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ &lt;b&gt;페이징인데도 전체 조회에 가까운 비용 발생&lt;/b&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 개선 방식 – CTE로 PK만 먼저 조회 (실무형 조건 기준)&lt;/h2&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;PK 조건 ❌&lt;/li&gt;&lt;li&gt;기간 조건 (DT FROM ~ TO)&lt;/li&gt;&lt;li&gt;공통코드 조건 (COMM_CD)&lt;/li&gt;&lt;li&gt;상태값 (USE_YN, STATUS 등)&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;PK로 바로 거르는 구조가 아니다.&lt;/b&gt;&lt;br&gt;그래서 더더욱 SELECT *(모든 컬럼 또는 불필요한 컬럼들) 페이징은 비효율적이다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 상황&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;테이블: EMP_LOG&lt;/li&gt; 
 &lt;li&gt;데이터 건수: 100만 건 이상&lt;/li&gt; 
 &lt;li&gt;사용 화면: 전 직원 공통 조회 화면&lt;/li&gt; 
 &lt;li&gt;조회 조건 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;등록일자 기간&lt;/li&gt; 
   &lt;li&gt;업무구분 코드&lt;/li&gt; 
   &lt;li&gt;사용 여부&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;Step 1. 조건 + 정렬 기준으로 PK만 페이징&lt;/h3&gt;&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;WITH PAGE_PK AS (
    SELECT  EMP_ID
    FROM    EMP_LOG
    WHERE   REG_DT BETWEEN :fromDt AND :toDt
    AND     COMM_CD = :commCd
    AND     USE_YN = 'Y'
    ORDER BY REG_DT DESC, EMP_ID
    OFFSET  :offset ROWS FETCH NEXT :pageSize ROWS ONLY
)
SELECT  E.EMP_ID,
        E.EMP_NM,
        E.DEPT_CD,
        E.COMM_CD,
        E.REG_DT,
        E.STATUS
FROM    EMP_LOG E
JOIN    PAGE_PK P
        ON E.EMP_ID = P.EMP_ID
ORDER BY E.REG_DT DESC;
&lt;/code&gt;&lt;/pre&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 이 방식이 더 효과적인가 (비 PK 조건 기준)&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 비 PK 조건에서도 인덱스 효율 극대화&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;REG_DT, COMM_CD, USE_YN 은 조회 화면에서 가장 흔한 조건&lt;/li&gt;&lt;li&gt;이 컬럼들 + PK로 &lt;b&gt;복합 인덱스 구성 가능&lt;/b&gt;&lt;/li&gt;&lt;/ul&gt;&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE INDEX IDX_EMP_LOG_01
ON EMP_LOG (REG_DT DESC, COMM_CD, USE_YN, EMP_ID);
&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ CTE 단계에서 &lt;b&gt;Index Range Scan + 정렬 최소화&lt;/b&gt; 가능&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) PK는 &quot;식별용&quot;으로만 사용&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;PK는 조건용이 아니라 &lt;b&gt;ROW 식별자 역할&lt;/b&gt;&lt;/li&gt;&lt;li&gt;상세 조회는 반드시 PK 조인으로 수행&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ 대용량 조건 필터링 + 소량 데이터 조회 분리&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 실제 화면 페이징 흐름과 동일&lt;/h3&gt;&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;&lt;li&gt;조건으로 대상 집합 결정&lt;/li&gt;&lt;li&gt;최신순 / 기준순 정렬&lt;/li&gt;&lt;li&gt;화면에 보여줄 PK n건 결정&lt;/li&gt;&lt;li&gt;그 PK의 상세 정보만 조회&lt;/li&gt;&lt;/ol&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ &lt;b&gt;사용자 화면 로직과 DB 처리 흐름이 일치&lt;/b&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 이 방식이 더 빠른가?&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) 불필요한 컬럼 I/O 제거&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;CTE 단계에서는 &lt;b&gt;PK 컬럼만 조회&lt;/b&gt;&lt;/li&gt; 
 &lt;li&gt;PK는 보통 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;크기가 작고&lt;/li&gt; 
   &lt;li&gt;인덱스에 포함되어 있음&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ &lt;b&gt;Index Scan만으로 페이징 처리 가능&lt;/b&gt;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) 정렬 대상 데이터 최소화&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;정렬은 CTE 단계에서 &lt;b&gt;PK + 정렬 컬럼만으로 수행&lt;/b&gt;&lt;/li&gt;&lt;li&gt;대용량 컬럼(CLOB, 긴 VARCHAR)은 정렬 대상에서 제외&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ Sort 비용 급감&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) 실제 데이터 조회는 &quot;필요한 ROW만&quot;&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;두 번째 SELECT에서는 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;이미 PK가 n건으로 줄어든 상태&lt;/li&gt; 
   &lt;li&gt;조인 대상이 매우 작음 (ex. 20~50건)&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ Table Access 비용이 거의 무시 가능한 수준&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4) 하루 종일 호출되는 화면에서 효과 극대화&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;전 직원 공통 사용 화면&lt;/li&gt;&lt;li&gt;짧은 조회 쿼리가 수천~수만 번 호출됨&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;→ &lt;b&gt;쿼리 1회당 50ms → 5ms만 줄어도 전체 DB 부하는 큰 차이&lt;/b&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;TABLE FULL SCAN 또는 대량 TABLE ACCESS&lt;/li&gt;&lt;li&gt;SORT 영역 사용량 큼&lt;/li&gt;&lt;li&gt;BUFFER CACHE 점유 증가&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;개선 방식&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;INDEX RANGE SCAN (PK 또는 정렬 인덱스)&lt;/li&gt;&lt;li&gt;소량 ROW 조인&lt;/li&gt;&lt;li&gt;SORT 대상 최소화&lt;/li&gt;&lt;/ul&gt;&lt;hr 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;1) 정렬 컬럼 + PK 인덱스 필수&lt;/h3&gt;&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 예시
CREATE INDEX IDX_EMP_LOG_01
ON EMP_LOG (REG_DT DESC, EMP_ID);
&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;정렬 컬럼 + PK 조합 인덱스가 없으면 효과 반감&lt;/li&gt;&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) OFFSET이 너무 큰 경우&lt;/h3&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
 &lt;li&gt;OFFSET 100,000 이상이면 이 방식도 느려질 수 있음&lt;/li&gt; 
 &lt;li&gt;가능하면 
  &lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt; 
   &lt;li&gt;&lt;b&gt;Keyset Pagination (마지막 PK 기준 조회)&lt;/b&gt; 고려&lt;/li&gt; 
  &lt;/ul&gt; &lt;/li&gt; 
&lt;/ul&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3) CTE 대신 Inline View도 동일한 원리&lt;/h3&gt;&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT  E.*
FROM    EMP_LOG E
JOIN (
    SELECT EMP_ID
    FROM EMP_LOG
    WHERE USE_YN = 'Y'
    ORDER BY REG_DT DESC
    OFFSET :offset ROWS FETCH NEXT :pageSize ROWS ONLY
) P
ON E.EMP_ID = P.EMP_ID;
&lt;/code&gt;&lt;/pre&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;대용량 페이징 조회에서 병목은 &lt;b&gt;&quot;조회 건수&quot;가 아니라 &quot;읽는 컬럼의 양&quot;&lt;/b&gt;&lt;/li&gt;&lt;li&gt;PK만 먼저 페이징 → 상세 컬럼은 필요한 만큼만 조회&lt;/li&gt;&lt;li&gt;하루 종일 사용되는 공통 화면일수록 효과가 크다&lt;/li&gt;&lt;/ul&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이징 = 화면 단위 조회이지, 컬럼까지 페이징할 필요는 없다.&lt;/b&gt;&lt;/p&gt; 
&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;실무 DB 튜닝에서 가장 체감이 컸던 패턴 중 하나였다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>실무</category>
      <category>CTE활용</category>
      <category>DB튜닝</category>
      <category>SQL튜닝</category>
      <category>대규모데이터</category>
      <category>대용량쿼리</category>
      <category>백엔드성능튜닝</category>
      <category>실무DB</category>
      <category>인덱스설계</category>
      <category>쿼리성능개선</category>
      <category>페이징처리</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/118</guid>
      <comments>https://2minmin2.tistory.com/118#entry118comment</comments>
      <pubDate>Sun, 4 Jan 2026 01:05:33 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 외부 API URL 호출 구현(RestTemplate / WebClient) | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/116</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;배치와 온라인 시스템이 분리되어 있어서, 스프링 배치에서 온라인 API를 호출해야 되는 상황이 있었다.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;타 시스템을 호출하기 위해 사용했던 방법을 적어보려고 한다.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;API 호출하기에 많은 방법들이 있지만, Spring에서 제공하는 http 클라이언트를 사용하여 API를 호출하려고 한다.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;대표적으로, RestTemplate와 WebClient가 있다.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;POST MAN으로 테스트하듯 API 호출할 수 있으며, ui없이 java로만 요청/응답 파싱 후 처리하는 느낌이었다.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;1. 실무에서 외부 API URL 호출 구현 방법 - 대표적인 방법들&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;(Java 기준, 백엔드에서 외부 API 호출)&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;1) HttpURLConnection (JDK 기본)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;장점: 라이브러리 추가 필요 없음.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;단점: 코드가 장황하고 불편함. 커넥션/스트림 관리 직접 해야 함. 유지보수성 떨어짐.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;용도: 거의 안 씀. 정말 경량, 의존성 없는 상황에서만 사용.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;2) RestTemplate (Spring 전통)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;장점: Spring에서 기본적으로 제공, 사용법 간단, ResponseEntity 등 스프링의 편의성 활용 가능.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;단점: 동기 방식(Non-blocking 아님), 스프링 5 이후로는 권장 X(Deprecated 아님, 유지보수만).&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;용도: 실무에서 가장 많이 씀(특히 스프링 4,5 환경), 비동기 필요 없으면 OK.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;3) WebClient (Spring 5 이후, Reactive)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;장점: 비동기/논블로킹 지원, 최신 방식, 응답 스트림 처리에 강점.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;단점: 러닝커브, 복잡해질 수 있음, Spring WebFlux 필요.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;용도: 비동기 처리, 대량 호출, 성능이슈 있을 때.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;h3 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;4) 외부 라이브러리(OkHttp, Apache HttpClient 등)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;장점: 범용성, 기능 풍부, 커넥션 풀/비동기 등 세부 설정 유리.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;단점: 스프링과 통합 불편할 수 있음, 추가 의존성 필요.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;용도: 스프링 안 쓰는 프로젝트, 특별히 고성능/특정 기능 요구할 때.&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;h2 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;2. RestTemplate 예시 (실제 코드)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Map;

@Service
public class ExternalApiService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final RestTemplate restTemplate = new RestTemplate();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public String callExternalApi(String url, Map&amp;lt;String, String&amp;gt; paramMap, String token) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 1. 헤더 세팅
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpHeaders headers = new HttpHeaders();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // x-www-form-urlencoded
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.setBearerAuth(token); // Authorization: Bearer {token}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 또는 headers.add(&quot;Authorization&quot;, &quot;Bearer &quot; + token);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 2. 바디 생성 (MultiValueMap으로 변환)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MultiValueMap&amp;lt;String, String&amp;gt; body = new LinkedMultiValueMap&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;body.setAll(paramMap);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 3. 엔티티 생성
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HttpEntity&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; entity = new HttpEntity&amp;lt;&amp;gt;(body, headers);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// 4. POST 호출
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ResponseEntity&amp;lt;String&amp;gt; response = restTemplate.exchange(
&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;&amp;nbsp;url,
&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;&amp;nbsp;HttpMethod.POST,
&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;&amp;nbsp;entity,
&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;&amp;nbsp;String.class
&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;&amp;nbsp;return response.getBody();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;위 코드는 예시이며, HttpEntity에 바디와 헤더를 담아서 요청만 하면 된다.&lt;br&gt;중요하게 확인해야 될 부분은 &lt;br&gt;&lt;br&gt;&lt;b&gt;1. &lt;/b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;요청타입/응답타입 (온라인 웹 서비스가 있다면 API 요청하고 개발자도구에서 네트워크 들어가서 바로 알 수 있음.)&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;예를 들어, 나는 요청타입이 JSON이 아니라 x-www-form-urlencoded 타입이어서 LinkedMultiValueMap에 담아 보내줬다.&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;2. &lt;/b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;액세스토큰 값 헤더에 넣어주기(필요시) -&amp;gt; 토큰값을 가져와서 헤더타입에 맞게 넣어줘야 됨.&lt;/b&gt;&lt;/span&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;(URL 지정은, 테스트하면서 진행할거라 공통적인 url인 로컬, 개발, 운영&amp;nbsp;&amp;nbsp;url을 상수로 박아둬서 LOCAL_URL, DEV_URL, PROD_URL 로 호출하고 뒤에 호출 경로 넣는 방식으로 두면 편함)&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;b&gt;3. &lt;/b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;응답 검증/타입에 따라 파싱&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;위에 절차만 진행하면 RestTemplate으로 API호출이 쉽게 가능하다.&lt;br&gt;&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;3. WebClient 예시 (위와 동일한 기능)&lt;/b&gt;&lt;/h3&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;Java&quot; data-ke-language=&quot;Java&quot;&gt;&lt;code&gt;import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Map;

@Service
public class ExternalApiService {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private final WebClient webClient = WebClient.builder().build();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public String callExternalApi(String url, Map&amp;lt;String, String&amp;gt; paramMap, String token) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// x-www-form-urlencoded 바디 준비
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MultiValueMap&amp;lt;String, String&amp;gt; body = new LinkedMultiValueMap&amp;lt;&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;body.setAll(paramMap);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// POST 요청
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return webClient.post()
&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;&amp;nbsp;.uri(url)
&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;&amp;nbsp;.contentType(MediaType.APPLICATION_FORM_URLENCODED)
&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;&amp;nbsp;.header(&quot;Authorization&quot;, &quot;Bearer &quot; + token)
&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;&amp;nbsp;.bodyValue(body)
&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;&amp;nbsp;.retrieve()
&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;&amp;nbsp;.bodyToMono(String.class)
&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;&amp;nbsp;.block(); // 동기 결과 반환
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;참고로&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;웹클라이언트는 스프링5+ 버전 이후로도 관리되는 http 클라이언트라서&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;span style=&quot;background-color: #9feec3;&quot;&gt;&lt;b&gt;RestTemplate(deprecated)와 비교한다면 WebClient를 사용하는 것을 권장한다고 공식문서에 나와있다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;span style=&quot;background-color: #9feec3;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;span style=&quot;background-color: #9feec3;&quot;&gt;&lt;b&gt;없어진다는 건 아니며, 권장하지 않는다고 한다.&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt; (추가 버전 업데이트를 하진 않을 예정인듯?)&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;그리고 위와 같이 웹클라이언트는 메서드체이닝 방식으로 (빌더패턴느낌) 가독성이 너무 좋아서&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;&lt;br&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Noto Sans Demilight, Noto Sans KR;&quot;&gt;&lt;b&gt;굳이 RestTemplate을 사용할 것 같지는 않다.&lt;/b&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;/p&gt;</description>
      <category>restTemplate</category>
      <category>webclient</category>
      <category>동기</category>
      <category>레스트템플릿</category>
      <category>비동기</category>
      <category>스프링 api 호출</category>
      <category>외부 API</category>
      <category>웹클라이언트</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/116</guid>
      <comments>https://2minmin2.tistory.com/116#entry116comment</comments>
      <pubDate>Tue, 5 Aug 2025 15:30:43 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] @Transactional과 Exception 처리 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/115</link>
      <description>&lt;h2 data-end=&quot;223&quot; data-start=&quot;197&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. @Transactional이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;332&quot; data-start=&quot;225&quot; data-ke-size=&quot;size16&quot;&gt;스프링에서 데이터베이스의 &lt;b&gt;트랜잭션&lt;/b&gt; 처리를 위해 사용하는 어노테이션이다.&lt;br /&gt;트랜잭션이란 한 번에 수행되어야 할 작업의 단위를 의미하고, 모두 성공하면 커밋, 하나라도 실패하면 롤백된다.&lt;/p&gt;
&lt;p data-end=&quot;332&quot; data-start=&quot;225&quot; data-ke-size=&quot;size16&quot;&gt;트랜잭셔널 어노테이션은 보통 http 요청에서 CRUD 기능에 따라 옵션을 설정하여 사용한다.&lt;br /&gt;예를 들어, GET 요청은 조회 시 대부분 사용되기에 @Transactional(readOnly = true)를 사용한다.&lt;br /&gt;데이터는 소중하기 때문에, 삭제되거나 잘못된 작업에 의해 롤백되지 않으면 심각한 오류를 초래할 수도 있다.&lt;/p&gt;
&lt;p data-end=&quot;332&quot; data-start=&quot;225&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;365&quot; data-start=&quot;334&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 롤백 조건 &amp;ndash; 왜 런타임 익셉션만 롤백할까?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;529&quot; data-start=&quot;367&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;스프링의 기본 정책은 &lt;b&gt;Unchecked Exception&lt;/b&gt;(즉, RuntimeException과 그 하위) 발생 시 자동 롤백하고,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Checked Exception&lt;/b&gt;(즉, Exception을 상속하지만 RuntimeException이 아닌 예외) 발생 시 롤백하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;614&quot; data-start=&quot;531&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;573&quot; data-start=&quot;531&quot;&gt;즉, throw new RuntimeException() &amp;rarr; 롤백&lt;/li&gt;
&lt;li data-end=&quot;614&quot; data-start=&quot;574&quot;&gt;throw new Exception() &amp;rarr; 기본적으로 롤백 안 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;777&quot; data-start=&quot;616&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;br /&gt;실무에서 RuntimeException은 예상 못 한 치명적인 상황(프로그래밍 실수, DB 제약 위반 등),&lt;br /&gt;Checked Exception은 비즈니스적으로 처리할 수 있는 예외(예: 네트워크 실패, 특정 비즈니스 검증 실패)로 설계한 것이 기본이기 때문이다.&lt;/p&gt;
&lt;p data-end=&quot;777&quot; data-start=&quot;616&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;777&quot; data-start=&quot;616&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;롤백이 필요한 실수 예시&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1751706076283&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class OrderService {
    @Transactional
    public void saveOrder(Order order) {
        orderRepository.save(order);
        // 아래에서 런타임 예외 발생
        if (order.getAmount() &amp;lt; 0) {
            throw new IllegalArgumentException(&quot;금액 오류&quot;);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;위 코드에서 IllegalArgumentException(런타임 익셉션)이 발생하면, DB에 저장된 것도 롤백된다(=실제로는 DB에 반영되지 않음)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Checked Exception 발생 시 롤백이 안되는 예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751706135715&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void process() throws IOException {
    // 비즈니스 처리
    throw new IOException(&quot;파일 오류&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;이 경우에도 롤백이 &lt;b&gt;안 됨&lt;/b&gt;&amp;nbsp;(별도 옵션 지정 필요)&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;b&gt;롤백 조건 바꾸기 (실무에서 자주 씀)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 Checked Exception이 발생해도 롤백해야 하는 경우가 많다.&lt;/p&gt;
&lt;pre id=&quot;code_1751706168517&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 doSomething() throws Exception {
    // ... 코드
    throw new Exception(&quot;예외 발생&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;(rollbackFor = Exception.class) 별도의 옵션을 넣어, 예외 부모 클래스의 예외가 터지면 롤백 시키는 경우로 사용되기도 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;1701&quot; data-start=&quot;1676&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서 주의할 점 &amp;amp; 흔한 실수&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2142&quot; data-start=&quot;1703&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1777&quot; data-start=&quot;1703&quot;&gt;&lt;b&gt;트랜잭션 범위 확인&lt;/b&gt;&lt;br /&gt;서비스 계층(Service)에 주로 적용, Repository/DAO에는 직접 붙이지 않는다.&lt;/li&gt;
&lt;li data-end=&quot;1865&quot; data-start=&quot;1779&quot;&gt;&lt;b&gt;self-invocation 문제&lt;/b&gt;&lt;br /&gt;같은 클래스 내에서 트랜잭셔널 메서드 끼리 호출하면, 프록시가 적용 안 돼 트랜잭션이 동작하지 않음.&lt;/li&gt;
&lt;li data-end=&quot;2142&quot; data-start=&quot;1867&quot;&gt;&lt;b&gt;예외를 try-catch 후 삼키거나, catch에서만 처리하고 다시 throw 안 하면 롤백되지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751706189987&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void doJob() {
    try {
        // ... 코드
    } catch (Exception e) {
        log.error(&quot;에러&quot;, e);
        // throw e;    // 이걸 해줘야 롤백된다!
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;2165&quot; data-start=&quot;2149&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론 및 실무 팁&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2525&quot; data-start=&quot;2167&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2210&quot; data-start=&quot;2167&quot;&gt;예상치 못한 에러, &lt;span style=&quot;background-color: #f6e199;&quot;&gt;반드시 롤백되어야 하는 로직에는 런타임 예외&lt;/span&gt;를 던짐.&lt;/li&gt;
&lt;li data-end=&quot;2309&quot; data-start=&quot;2211&quot;&gt;비즈니스 검증 실패 등에서 롤백이 필요하면 커스텀 런타임 예외(예: InvalidOrderException extends RuntimeException)를 만들어 사용.&lt;/li&gt;
&lt;li data-end=&quot;2399&quot; data-start=&quot;2310&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;Checked Exception에도 롤백이 필요하면 @Transactional(rollbackFor = Exception.class) 옵션 반드시 명시&lt;/span&gt;.&lt;/li&gt;
&lt;li data-end=&quot;2463&quot; data-start=&quot;2400&quot;&gt;트랜잭션은 &lt;b&gt;최대한 짧게!&lt;/b&gt; (트랜잭션 안에 네트워크 호출, 외부 API 호출 등은 넣지 않는 것이 원칙)&lt;/li&gt;
&lt;li data-end=&quot;2525&quot; data-start=&quot;2464&quot;&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;br /&gt;그래서 만들어져 있는 예외에 따른 사용은 개발자가 어느정도 알고 있어야 예기치 못한 오류를 막는다...&lt;br /&gt;그래서 나는 파일처리와 같은 다른 내용의 메서드가 아니면 RuntimeException과 Exception 에외처리로 감싸고 @Transactional(rollbackFor = Exception.class) 옵션을 지정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>실무</category>
      <category>@Transactional</category>
      <category>RestAPI</category>
      <category>데이터정합성</category>
      <category>서버개발자</category>
      <category>스프링부트</category>
      <category>실무개발</category>
      <category>예외처리</category>
      <category>자바개발자</category>
      <category>트랜잭션</category>
      <category>트랜잭션옵션</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/115</guid>
      <comments>https://2minmin2.tistory.com/115#entry115comment</comments>
      <pubDate>Sat, 5 Jul 2025 18:15:40 +0900</pubDate>
    </item>
    <item>
      <title>[FE] jQuery 반복문($.each) 사용 시 주의점 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/114</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;jQuery 라이브러리 사용 중, 아래와 같은 에러가 발생했다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1747790043686&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SyntaxError: Illegal continue statement: no surrounding iteration statement&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;continue가 문제라는 것 같았다. 사실 continue문을 사용하는 것을 지양하라고 듣긴 했는데 위 에러는 처음 보는 에러여서 검색해보니, &lt;b&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;jQuery $.each 또는 foreach 문에서는 continue, break, yield 문을 사용하지 못 한다고 한다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;125&quot; data-start=&quot;72&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;jQuery $.each() 반복문에서 continue 사용 불가 이유&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;288&quot; data-start=&quot;157&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;244&quot; data-start=&quot;157&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JavaScript에서 continue는 &lt;b&gt;전통적인 반복문&lt;/b&gt;(for, while, do...while) 내에서만 작동하는 제어문임.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747791047612&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for(let i=0; i&amp;lt;5; i++) {
  if(i === 2) continue; // i가 2일 때는 아래 코드를 건너뛰고 다음 i로 넘어감
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;462&quot; data-start=&quot;432&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그런데 jQuery $.each()는?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;464&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;$.each()는 내부적으로 &lt;b&gt;자체 콜백 함수와 반복 제어 방식을 사용&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1747791082189&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$.each(arr, function(index, value) {
  if (value % 2 === 0) return true; // 또는 return;
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;464&quot; 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;675&quot; data-start=&quot;587&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 콜백 함수는 내부에서 반복을 제어하는 함수이며, &lt;b&gt;continue는 JavaScript 루프의 반복문 범위가 아니기 때문에 작동하지 않음.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;754&quot; data-start=&quot;676&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;즉, $.each() 안에서 continue를 쓰면 &lt;b&gt;SyntaxError가 발생하지 않지만, 반복이 건너뛰어지지 않음.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;512&quot; data-start=&quot;464&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;512&quot; data-start=&quot;464&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 156px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 6.31786%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;제어문&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.2481%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;반복문(for, while)에서 동작 여부 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.3876%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; $.each() 내부 콜백에서 사용 가능 여부 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7132%; height: 22px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 대체 방법 및 설명 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 6.31786%; height: 60px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; continue &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.2481%; height: 60px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.3876%; height: 60px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7132%; height: 60px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; return true; 또는 return; (다음 반복으로 건너뛰기) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 6.31786%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;break&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.2481%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.3876%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7132%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; return false; (반복 중단) &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 6.31786%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;return&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.2481%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;함수 내부에서 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.3876%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7132%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 콜백 함수 종료, true/false 반환으로 제어 가능 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 6.31786%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;throw&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.2481%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.3876%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;O&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7132%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 예외 발생, 정상 종료를 중단하고 에러 전달 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 6.31786%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; yield &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.2481%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 제너레이터 함수 내에서만 사용 가능 &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.3876%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;X&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.7132%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 해당 없음 &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>실무</category>
      <category>continue 사용법</category>
      <category>each return true</category>
      <category>foreach 반복제어</category>
      <category>javascript foreach</category>
      <category>jQuery each</category>
      <category>js 반복문</category>
      <category>반복문 제어문</category>
      <category>자바스크립트 팁</category>
      <category>콜백함수 제어</category>
      <category>프론트엔드 기초</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/114</guid>
      <comments>https://2minmin2.tistory.com/114#entry114comment</comments>
      <pubDate>Wed, 21 May 2025 10:36:57 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스프링배치 흐름 및 @Schedueld 어노테이션 사용 시 유의사항 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/113</link>
      <description>&lt;h2 data-end=&quot;90&quot; data-start=&quot;65&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배치(스프링배치)란?&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;149&quot; data-start=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;스프링 배치는 &lt;b&gt;대용량 데이터 처리&lt;/b&gt;에 특화된 프레임워크이다. &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;배치작업은 데이터를 실시간으로 처리하는게 아니라, 일괄적으로 모아서 한번에 처리하는 작업이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;149&quot; data-start=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt; &lt;br /&gt;&lt;b&gt;예를 들어,&amp;nbsp; 쇼핑몰(&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;온라인&lt;/span&gt; 웹서비스)에서 매일 매출액에 대한 금액을 조회하는 배치가 있다고 가정해보자.&lt;/b&gt;&lt;br /&gt;&lt;b&gt;온라인에서 일어나는 데이터변화에 따라 배치주기(ex, 매일 오전 6시)에 의해 배치가 돌게 된다. 온라인에서 사용자나 개발자가 직접 실행하는게 아닌, 통계/SMS전송/대용량CUD 처리 등을 위해 특화된 작업이다.&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;149&quot; data-start=&quot;92&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-start=&quot;65&quot; data-end=&quot;90&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;스프링 배치 흐름 설명&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;166&quot; data-start=&quot;151&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기본 구성 요소&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;450&quot; data-start=&quot;167&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;206&quot; data-start=&quot;167&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Job&lt;/b&gt;: 배치 단위 작업. 여러 Step을 포함할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;273&quot; data-start=&quot;207&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Step&lt;/b&gt;: 실제 처리 단위. 일반적으로 Reader &amp;rarr; Processor &amp;rarr; Writer 구조로 구성됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;346&quot; data-start=&quot;274&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;JobLauncher&lt;/b&gt;: Job 실행을 담당. 일반적으로 외부 트리거(@Scheduled, 컨트롤러 등)를 통해 호출됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;384&quot; data-start=&quot;347&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;JobRepository&lt;/b&gt;: 실행 이력, 상태 등을 저장.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;450&quot; data-start=&quot;385&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;JobBuilderFactory / StepBuilderFactory&lt;/b&gt;: Job/Step을 생성할 때 사용.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;464&quot; data-start=&quot;452&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실행 흐름&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;740&quot; data-start=&quot;465&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;521&quot; data-start=&quot;465&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JobLauncher가 Job을 실행 (run(Job, JobParameters)).&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;558&quot; data-start=&quot;522&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Job 내 정의된 여러 Step이 순차적으로 실행됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;698&quot; data-start=&quot;559&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;각 Step은 다음 흐름으로 동작:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;698&quot; data-start=&quot;587&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;625&quot; data-start=&quot;587&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ItemReader: 데이터 읽기 (DB, 파일, API 등)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;665&quot; data-start=&quot;629&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ItemProcessor: 읽은 데이터 가공 (선택 사항)&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;698&quot; data-start=&quot;669&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ItemWriter: 데이터를 저장하거나 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;740&quot; data-start=&quot;699&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JobRepository에 실행 상태 저장 (성공/실패/중단 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-end=&quot;795&quot; data-start=&quot;742&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;795&quot; data-start=&quot;744&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배치 작업은 &lt;b&gt;트랜잭션 단위&lt;/b&gt;로 끊어 실행되며, chunk 사이즈만큼 반복 처리된다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-end=&quot;800&quot; data-start=&quot;797&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-end=&quot;842&quot; data-start=&quot;802&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; font-size: 1.62em;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;842&quot; data-start=&quot;802&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;@Scheduled와 매개변수 사용 불가한 이유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1443&quot; data-start=&quot;1362&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@Scheduled는 단순히 &lt;b&gt;정해진 시간에 메서드를 호출하는 어노테이션&lt;/b&gt;이다. 하지만 &lt;b&gt;메서드에 파라미터가 있으면 실행되지 않는다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;1443&quot; data-start=&quot;1362&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;예를 들어,&lt;span style=&quot;background-color: #ffc9af;&quot;&gt; 잘못된 예시 코드&lt;/span&gt;를 보자면&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1747788843475&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(cron = &quot;0 0 * * * *&quot;)
public void scheduledJob(String input) {
    System.out.println(&quot;실행됨: &quot; + input);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;668&quot; data-start=&quot;525&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;616&quot; data-start=&quot;525&quot;&gt;IllegalStateException: Only no-arg methods may be annotated with @Scheduled 예외 발생&lt;/li&gt;
&lt;li data-end=&quot;668&quot; data-start=&quot;617&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;이유: @Scheduled 메서드는 &lt;b&gt;파라미터 없는(no-arg) 메서드만 허용&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1443&quot; data-start=&quot;1362&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1443&quot; data-start=&quot;1362&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #99cefa;&quot;&gt;&lt;b&gt;수정된 예시 코드&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1747788944387&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(cron = &quot;0 0 * * * *&quot;) // 매 정각마다 실행
public void scheduledJob() {
    System.out.println(&quot;정각마다 실행되는 작업&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값 void, 파라미터 없이 사용해야 된다.&lt;br /&gt;만약 파라미터가 필요하다면 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;파라미터 없이 실행하되 내부에서 값을 가져오는 방식&lt;/b&gt;&lt;/span&gt;으로 구현해야 한다.&lt;/p&gt;
&lt;h3 data-end=&quot;1454&quot; data-start=&quot;1445&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이유&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1601&quot; data-start=&quot;1455&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1516&quot; data-start=&quot;1455&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;@Scheduled는 &lt;b&gt;Spring의 TaskScheduler가 리플렉션을 통해 메서드를 호출&lt;/b&gt;함.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1545&quot; data-start=&quot;1517&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 호출은 매개변수 없는 메서드만 지원한다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1601&quot; data-start=&quot;1546&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 @Scheduled 메서드는 public void method() 형식이어야 함.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1615&quot; data-start=&quot;1603&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해결 방법&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1748&quot; data-start=&quot;1616&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1670&quot; data-start=&quot;1616&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;파라미터가 필요한 경우, 내부에서 값을 조회하거나 JobParameter로 넘겨서 배치 실행.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1748&quot; data-start=&quot;1671&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또는 @Scheduled 메서드에서 JobLauncher.run()을 호출하고, 그 안에서 JobParameters를 전달.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>실무</category>
      <category>java개발</category>
      <category>noargsonly</category>
      <category>scheduled</category>
      <category>SpringBatch</category>
      <category>springboot</category>
      <category>voidmethod</category>
      <category>개발기록</category>
      <category>문제해결</category>
      <category>배치작업</category>
      <category>스프링제한사항</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/113</guid>
      <comments>https://2minmin2.tistory.com/113#entry113comment</comments>
      <pubDate>Wed, 21 May 2025 09:59:39 +0900</pubDate>
    </item>
    <item>
      <title>[DB] CONNECT BY와 WITH RECURSIVE 계층형 데이터 조회 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/111</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;내용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신청 마스터테이블이 있을 때, 키값은 순번(신청순번 시퀀스)SN이 계속 쌓인다. UP_SN컬럼도 있다. 해당 컬럼은 상위 이력을 보기 위해 존재하는 컬럼이다. 신청순번/상위신청순번이 있다고 가정.&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;- 처음 신청 시에는 상위 순번이 없음 (UP_SN : NULL)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 신청 건에서 반려처리되면 재신청이 가능 (재신청 시 새로운 순번으로 신청내용 INSERT 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 반려된 건에서 재신청 시 SN이 새로운 신청 건의 UP_SN 값으로 들어감.&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;120&quot; data-start=&quot;104&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;120&quot; data-start=&quot;104&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;예제 데이터&lt;/b&gt;&lt;/h2&gt;
&lt;p data-end=&quot;144&quot; data-start=&quot;121&quot; data-ke-size=&quot;size16&quot;&gt;디테일테이블에는 신청 내역이 있고,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;212&quot; data-start=&quot;145&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;186&quot; data-start=&quot;145&quot;&gt;&lt;b&gt;반려되면 새로운 신청(up_sn이 이전 sn을 참조)&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;212&quot; data-start=&quot;187&quot;&gt;&lt;b&gt;승인되면 더 이상 재신청이 없음&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;br /&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 90px;&quot; border=&quot;1&quot; data-end=&quot;450&quot; data-start=&quot;214&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style14&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sn&lt;/td&gt;
&lt;td&gt;up_sn&lt;/td&gt;
&lt;td&gt;상태&lt;/td&gt;
&lt;td&gt;신청자&lt;/td&gt;
&lt;td&gt;신청일자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;330&quot; data-start=&quot;291&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;NULL&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;반려&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;홍길동&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2024-03-10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;370&quot; data-start=&quot;331&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;반려&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;홍길동&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2024-03-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;410&quot; data-start=&quot;371&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;승인&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;홍길동&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2024-03-15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;450&quot; data-start=&quot;411&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;NULL&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;승인&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;이영희&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2024-03-16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;455&quot; data-start=&quot;452&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;490&quot; data-start=&quot;457&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1 data-end=&quot;490&quot; data-start=&quot;457&quot;&gt;&lt;b&gt;&amp;nbsp;1. CONNECT BY (Oracle)&lt;/b&gt;&lt;/h1&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1742291617688&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT 
    CONNECT_BY_ROOT sn AS 최초신청번호, 
    sn AS 현재신청번호,
    up_sn AS 이전신청번호,
    상태
FROM 디테일테이블
START WITH up_sn IS NULL
CONNECT BY PRIOR sn = up_sn
ORDER BY 최초신청번호 DESC, 현재신청번호 DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;743&quot; data-start=&quot;727&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실행 결과&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 90px;&quot; border=&quot;1&quot; data-end=&quot;1019&quot; data-start=&quot;744&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;최초신청번호&lt;/td&gt;
&lt;td&gt;현재신청번호&lt;/td&gt;
&lt;td&gt;이전신청번호&lt;/td&gt;
&lt;td&gt;상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;875&quot; data-start=&quot;828&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;NULL&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;반려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;923&quot; data-start=&quot;876&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;반려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;971&quot; data-start=&quot;924&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;승인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot; data-end=&quot;1019&quot; data-start=&quot;972&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;NULL&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;승인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;1047&quot; data-start=&quot;1021&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CONNECT BY 설명&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1346&quot; data-start=&quot;1048&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1110&quot; data-start=&quot;1048&quot;&gt;START WITH up_sn IS NULL&lt;br /&gt;&amp;rarr; 최초 신청(up_sn이 없는 경우)을 찾음&lt;/li&gt;
&lt;li data-end=&quot;1195&quot; data-start=&quot;1111&quot;&gt;CONNECT BY PRIOR sn = up_sn&lt;br /&gt;&amp;rarr; &lt;b&gt;이전 신청(sn)을 참조하는 데이터(up_sn)를 계속 따라감&lt;/b&gt;&lt;/li&gt;
&lt;li data-end=&quot;1270&quot; data-start=&quot;1196&quot;&gt;CONNECT_BY_ROOT sn AS 최초신청번호&lt;br /&gt;&amp;rarr; 현재 신청(sn)이 속한 최초 신청(sn)을 찾음&lt;/li&gt;
&lt;li data-end=&quot;1346&quot; data-start=&quot;1271&quot;&gt;ORDER BY 최초신청번호 DESC, 현재신청번호 DESC&lt;br /&gt;&amp;rarr; &lt;b&gt;최신 최초 신청을 기준으로 최신 신청부터 정렬&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1446&quot; data-start=&quot;1348&quot;&gt;Oracle에서는 CONNECT BY를 사용하면 매우 간단하고 빠르게 계층 쿼리를 실행할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;1446&quot; data-start=&quot;1348&quot;&gt;재귀 없이도 부모-자식 관계를 쉽게 조회 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1451&quot; data-start=&quot;1448&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1501&quot; data-start=&quot;1453&quot;&gt;&amp;nbsp;&lt;/h1&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;h1 data-end=&quot;1501&quot; data-start=&quot;1453&quot;&gt;&lt;b&gt;2. WITH RECURSIVE (MySQL, PostgreSQL)&lt;/b&gt;&lt;/h1&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1742291679019&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH RECURSIVE history AS (
    -- 최초 신청 데이터를 가져옴
    SELECT 
        sn AS 최초신청번호, 
        sn AS 현재신청번호,
        up_sn AS 이전신청번호,
        상태
    FROM 디테일테이블
    WHERE up_sn IS NULL

    UNION ALL

    -- 재귀적으로 up_sn을 따라가며 연결
    SELECT 
        h.최초신청번호,
        d.sn AS 현재신청번호,
        d.up_sn AS 이전신청번호,
        d.상태
    FROM 디테일테이블 d
    JOIN history h ON d.up_sn = h.현재신청번호
)
SELECT * FROM history ORDER BY 최초신청번호 DESC, 현재신청번호 DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;1982&quot; data-start=&quot;1953&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1982&quot; data-start=&quot;1953&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;실행 결과&lt;/b&gt; (Oracle과 동일)&lt;/h3&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;2258&quot; data-start=&quot;1983&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody data-end=&quot;2258&quot; data-start=&quot;2067&quot;&gt;
&lt;tr&gt;
&lt;td&gt;최초신청번호&lt;/td&gt;
&lt;td&gt;현재신청번호&lt;/td&gt;
&lt;td&gt;이전신청번호&lt;/td&gt;
&lt;td&gt;상태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2114&quot; data-start=&quot;2067&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;반려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2162&quot; data-start=&quot;2115&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;반려&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2210&quot; data-start=&quot;2163&quot;&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;승인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2258&quot; data-start=&quot;2211&quot;&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;승인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;2290&quot; data-start=&quot;2260&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;2290&quot; data-start=&quot;2260&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WITH RECURSIVE 설명&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2628&quot; data-start=&quot;2291&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2363&quot; data-start=&quot;2291&quot;&gt;&lt;b&gt;초기 데이터 (WHERE up_sn IS NULL)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2363&quot; data-start=&quot;2334&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2363&quot; data-start=&quot;2334&quot;&gt;최초 신청(up_sn이 없는 경우)을 찾음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2468&quot; data-start=&quot;2364&quot;&gt;&lt;b&gt;재귀적으로 up_sn을 따라가면서 연결 (JOIN history h ON d.up_sn = h.현재신청번호)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2468&quot; data-start=&quot;2439&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2468&quot; data-start=&quot;2439&quot;&gt;up_sn이 현재신청번호와 같으면 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2558&quot; data-start=&quot;2469&quot;&gt;&lt;b&gt;최초 신청번호를 유지 (h.최초신청번호)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2558&quot; data-start=&quot;2506&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2558&quot; data-start=&quot;2506&quot;&gt;CONNECT_BY_ROOT 대신, &lt;b&gt;재귀 쿼리에서 최초 신청번호를 계속 유지&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;2628&quot; data-start=&quot;2559&quot;&gt;ORDER BY 최초신청번호 DESC, 현재신청번호 DESC
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2628&quot; data-start=&quot;2603&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2628&quot; data-start=&quot;2603&quot;&gt;&lt;b&gt;최신 최초 신청을 기준으로 정렬&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2747&quot; data-start=&quot;2630&quot;&gt;MySQL과 PostgreSQL에서는 WITH RECURSIVE를 사용해야 계층 데이터를 조회할 수 있음&lt;/li&gt;
&lt;li data-end=&quot;2747&quot; data-start=&quot;2630&quot;&gt;재귀적으로 up_sn을 따라가면서 연결하여 같은 결과를 얻을 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2752&quot; data-start=&quot;2749&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;2798&quot; data-start=&quot;2754&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1 data-end=&quot;2798&quot; data-start=&quot;2754&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1 data-end=&quot;2798&quot; data-start=&quot;2754&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1 data-end=&quot;2798&quot; data-start=&quot;2754&quot;&gt;&amp;nbsp;&lt;/h1&gt;
&lt;h1 data-end=&quot;2798&quot; data-start=&quot;2754&quot;&gt;&lt;b&gt;CONNECT BY vs WITH RECURSIVE 비교&lt;/b&gt;&lt;/h1&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3170&quot; data-start=&quot;2799&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style15&quot;&gt;
&lt;tbody data-end=&quot;3170&quot; data-start=&quot;2923&quot;&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Oracle (CONNECT BY)&lt;/td&gt;
&lt;td&gt;MySQL / PostgreSQL (WITH RECURSIVE)&lt;span&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;2994&quot; data-start=&quot;2923&quot;&gt;
&lt;td&gt;&lt;b&gt;간결함&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;매우 간단함 (CONNECT BY 한 줄)&lt;/td&gt;
&lt;td&gt;상대적으로 복잡 (WITH RECURSIVE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3043&quot; data-start=&quot;2995&quot;&gt;
&lt;td&gt;&lt;b&gt;속도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;빠름 (최적화된 계층 쿼리)&lt;/td&gt;
&lt;td&gt;느릴 수 있음 (재귀 호출)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3110&quot; data-start=&quot;3044&quot;&gt;
&lt;td&gt;&lt;b&gt;이해하기 쉬움&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CONNECT_BY_ROOT로 최초 신청 찾기&lt;/td&gt;
&lt;td&gt;최초신청번호를 유지해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3170&quot; data-start=&quot;3111&quot;&gt;
&lt;td&gt;&lt;b&gt;지원 DB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Oracle 전용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MySQL / PostgreSQL 지원&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;3182&quot; data-start=&quot;3172&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3325&quot; data-start=&quot;3183&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3222&quot; data-start=&quot;3183&quot;&gt;Oracle에서는 CONNECT BY가 더 쉽고 빠름&lt;/li&gt;
&lt;li data-end=&quot;3276&quot; data-start=&quot;3223&quot;&gt;MySQL과 PostgreSQL에서는 WITH RECURSIVE를 사용해야 함&lt;/li&gt;
&lt;li data-end=&quot;3325&quot; data-start=&quot;3277&quot;&gt;결과는 동일하지만, Oracle이 계층형 데이터를 더 쉽게 다룰 수 있음&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>실무</category>
      <category>connect_by</category>
      <category>MySQL</category>
      <category>Oracle</category>
      <category>PostgreSQL</category>
      <category>SQL쿼리</category>
      <category>with_recursive</category>
      <category>계층형쿼리</category>
      <category>데이터베이스</category>
      <category>이력조회</category>
      <category>재귀쿼리</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/111</guid>
      <comments>https://2minmin2.tistory.com/111#entry111comment</comments>
      <pubDate>Sun, 23 Mar 2025 15:19:58 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 데이터 전달하기: Model, Map, DTO 비교와 최적 활용 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;몇개에 실무 프로젝트를 진행하면서 많이 배우고 느낀 것 중 하나는 데이터 전달 방식이다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-end=&quot;165&quot; data-start=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;실무에서는 데이터를 주고받을 때 여러 방식이 사용되는데, 주요 방식으로는 &lt;b&gt;Model 클래스 사용, Map&amp;lt;String, Object&amp;gt; 사용, DTO(Entity) 사용 등&lt;/b&gt;이 있다. 각각의 방식이 효율적인 경우와 장단점에 대해 생각해봤다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-end=&quot;192&quot; data-start=&quot;172&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;192&quot; data-start=&quot;172&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1. Model 클래스 사용&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;246&quot; data-start=&quot;193&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Model은 Spring MVC에서 &lt;b&gt;View와 데이터를 공유&lt;/b&gt;하는 용도로 사용&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-start=&quot;172&quot; data-end=&quot;192&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;주로&amp;nbsp;컨트롤러에서 화면(View)으로 데이터를 넘길 때 사용됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-start=&quot;172&quot; data-end=&quot;192&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Thymeleaf, JSP 같은 템플릿 엔진에서 활용됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1741876606097&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class UserController {
    @GetMapping(&quot;/user&quot;)
    public String getUser(Model model) {
        User user = new User(1, &quot;민민&quot;, &quot;minmin@example.com&quot;);
        model.addAttribute(&quot;user&quot;, user);
        return &quot;userView&quot;; // userView.html로 데이터 전달
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1741876663998&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class User {
    private int id;
    private String name;
    private String email;
    
    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;192&quot; data-start=&quot;172&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; background-color: #f6e199;&quot;&gt;모델 클래스 만들고 -&amp;gt; getter &amp;amp; setter 로 데이터 주고 받음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;995&quot; data-start=&quot;894&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;995&quot; data-start=&quot;905&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;947&quot; data-start=&quot;905&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Thymeleaf, JSP 같은 템플릿 엔진과 함께 사용하기 편리함.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;995&quot; data-start=&quot;950&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;키 값을 잘못 입력할 일이 없고, 정형화된 데이터 구조를 유지할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1074&quot; data-start=&quot;997&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1074&quot; data-start=&quot;1008&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1033&quot; data-start=&quot;1008&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 응답으로 사용하기에는 부적절함.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1074&quot; data-start=&quot;1036&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JSON 응답을 할 때는 DTO를 사용하는 것이 일반적임.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1113&quot; data-start=&quot;1081&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Map&amp;lt;String, Object&amp;gt; 사용&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;1148&quot; data-start=&quot;1114&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Map을 사용하면 &lt;b&gt;유연하게 데이터를 담을 수 있음&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1148&quot; data-start=&quot;1114&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;컬럼 개수가 일정하지 않거나, 동적으로 데이터를 담아야 할 때 사용됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1148&quot; data-start=&quot;1114&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;공통적으로 데이터를 전달할 때도 활용됨.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1741876774454&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/user&quot;)
public class UserController {
    @GetMapping(&quot;/info&quot;)
    public Map&amp;lt;String, Object&amp;gt; getUserInfo() {
        Map&amp;lt;String, Object&amp;gt; userInfo = new HashMap&amp;lt;&amp;gt;();
        userInfo.put(&quot;id&quot;, 1);
        userInfo.put(&quot;name&quot;, &quot;민민&quot;);
        userInfo.put(&quot;email&quot;, &quot;minmin@example.com&quot;);
        userInfo.put(&quot;age&quot;, 28); // 나중에 동적으로 추가할 수도 있음.

        return userInfo; // JSON 형태로 반환됨
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;모델 클래스(엔티티) 만들지 않아도 key-value로 담아서 데이터 전달 가능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1763&quot; data-start=&quot;1673&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1763&quot; data-start=&quot;1684&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1724&quot; data-start=&quot;1684&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;키-값 형태이기 때문에 원하는 데이터를 동적으로 추가할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1763&quot; data-start=&quot;1727&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 응답으로 사용할 경우 직관적이고 가볍게 사용 가능.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;1923&quot; data-start=&quot;1765&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1923&quot; data-start=&quot;1776&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1826&quot; data-start=&quot;1776&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;타입 안정성이 없음 &amp;rarr; 잘못된 타입을 넣어도 컴파일 단계에서 체크되지 않음.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1887&quot; data-start=&quot;1829&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;오타 등으로 인해 userInfo.get(&quot;email&quot;)을 사용할 때 문제가 발생할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1923&quot; data-start=&quot;1890&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;명확한 구조가 없어서 코드 가독성이 떨어질 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1958&quot; data-start=&quot;1930&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3. 엔티티(Entity) &amp;amp; DTO 사용&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-end=&quot;2017&quot; data-start=&quot;1959&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring Boot에서는 &lt;b&gt;DTO(Data Transfer Object) 패턴&lt;/b&gt;을 많이 사용함.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2017&quot; data-start=&quot;1959&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 응답을 JSON으로 줄 때 가장 많이 사용됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2100&quot; data-start=&quot;2020&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB와 연관된 엔티티(Entity)와 분리하여 사용해야 유지보수에 좋음.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;2135&quot; data-start=&quot;2116&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) 엔티티 (JPA 사용)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741876855401&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;users&quot;)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private String address;
    private int age;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 2) DTO (API 응답용)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741876873113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
public class UserDTO {
    private Long id;
    private String name;
    private String email;

    public UserDTO(User user) {
        this.id = user.getId();
        this.name = user.getName();
        this.email = user.getEmail();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 3) 컨트롤러에서 사용 &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741876885364&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/user&quot;)
public class UserController {
    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping(&quot;/{id}&quot;)
    public UserDTO getUserById(@PathVariable Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -&amp;gt; new RuntimeException(&quot;User not found&quot;));

        return new UserDTO(user);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;엔티티를 만들고 특정 메서드에서 필요한 전송 객체를 DTO에 담아줌. (로그인일 시 User의 id와 pw만 넣는 식)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3319&quot; data-start=&quot;3173&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3319&quot; data-start=&quot;3184&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3221&quot; data-start=&quot;3184&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DTO는 API 응답을 JSON으로 변환하는데 최적화됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;3251&quot; data-start=&quot;3224&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터 구조가 명확하여 유지보수가 용이함.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;3319&quot; data-start=&quot;3254&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Entity와 DTO를 분리하면 &lt;b&gt;JPA 연관관계와 무관하게 DTO로만 데이터를 전달&lt;/b&gt;할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;3420&quot; data-start=&quot;3321&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3420&quot; data-start=&quot;3332&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3372&quot; data-start=&quot;3332&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터를 담기 위해 클래스를 추가로 만들어야 해서 코드가 많아짐.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;3420&quot; data-start=&quot;3375&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;간단한 응답을 할 때도 DTO 클래스를 만들어야 하는 번거로움이 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-end=&quot;3425&quot; data-start=&quot;3422&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;3437&quot; data-start=&quot;3427&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3437&quot; data-start=&quot;3427&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-end=&quot;3437&quot; data-start=&quot;3427&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-end=&quot;3748&quot; data-start=&quot;3438&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;방법&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.3255%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;장점&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;단점&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.9303%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;추천 사용 시기&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3576&quot; data-start=&quot;3493&quot;&gt;
&lt;td style=&quot;width: 19.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Model 사용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.3255%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;View와 데이터 공유 용이&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 응답에는 적절하지 않음&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.9303%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;템플릿 엔진(Thymeleaf, JSP)에서 사용&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3669&quot; data-start=&quot;3577&quot;&gt;
&lt;td style=&quot;width: 19.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Map&amp;lt;String, Object&amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.3255%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;동적으로 데이터 추가 가능&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;타입 안정성이 없음, 키 값 오타 가능성&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.9303%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;응답 데이터 구조가 일정하지 않을 때&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;3748&quot; data-start=&quot;3670&quot;&gt;
&lt;td style=&quot;width: 19.4186%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DTO &amp;amp; Entity 사용&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.3255%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;유지보수 용이, API 응답 최적화&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 27.2093%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;클래스 추가 필요, 코드 증가&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.9303%;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 개발 시 권장&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3759&quot; data-start=&quot;3750&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;3759&quot; data-start=&quot;3750&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-is-last-node=&quot;&quot; data-is-only-node=&quot;&quot; data-end=&quot;3922&quot; data-start=&quot;3760&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3799&quot; data-start=&quot;3760&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Spring MVC의 View에서는 Model 사용.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;3863&quot; data-start=&quot;3800&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;API 응답에는 DTO를 사용하고, 가벼운 데이터는 Map&amp;lt;String, Object&amp;gt;를 활용.&lt;/span&gt;&lt;/li&gt;
&lt;li data-is-last-node=&quot;&quot; data-end=&quot;3922&quot; data-start=&quot;3864&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DB와 연관된 엔티티를 바로 반환하지 말고, DTO로 변환해서 전달하는 것이 유지보수에 좋음.&lt;/span&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span&gt;개인적인 생각&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Model 클래스는 불편함이 많은 것 같다. Map&amp;lt;String, Object&amp;gt;나 DTO&amp;amp;Entity의 장점은 명확하지만 Model 클래스 사용할 때가 가장 불편하게 느꼈다. 매퍼에 파라미터 타입을 그 클래스로 받으면 커스텀할 때 너무 제약적이라고 생각. (차라리 &lt;/span&gt;&lt;span&gt;Map&amp;lt;String, Object&amp;gt;가 더 유동적이고 가시성이 좋아보임)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Map&amp;lt;String, Object&amp;gt; 는 SI 프로젝트에서는 자주 사용되는 것을 볼 수 있었다. 규모가 크고 테이블과 제약조건이 많은 차세대 프로젝트 (기존에 ASIS 프로그램이 있음)는 사실 Model이나 Entity 만드는게 일이라고 생각하는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;이유로는 음.... 고객에 요구사항 변경(컬럼 삭제, 수정, 추가 등등)이 있을 수도 있고, 데이터는 정말 중요하고 체계적으로 잡아야해서?&amp;nbsp; 유지보수는 힘들지만 개발할 때 유용하기 때문에 많이 사용하는 것 같음. 근데 &lt;/span&gt;&lt;span&gt;위에 작성한 내용처럼, 컬럼에 대한 검증이 안 된다. &lt;/span&gt;&lt;span&gt;(오타나면 사고)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity &amp;amp; DTO는 API명세서가 명확할 때, 유지보수도 편하고 DTO에 커스텀해서 사용해서 사용하면 도메인에 따라 잘 나뉘어져 있어서 가장 좋은 것 같다고 생각이 든다.&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;&lt;/p&gt;</description>
      <category>실무</category>
      <category>Backend</category>
      <category>DTO</category>
      <category>java</category>
      <category>map</category>
      <category>model</category>
      <category>MVC패턴</category>
      <category>springboot</category>
      <category>SpringMVC</category>
      <category>데이터전달</category>
      <category>웹개발</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/110</guid>
      <comments>https://2minmin2.tistory.com/110#entry110comment</comments>
      <pubDate>Fri, 14 Mar 2025 23:25:09 +0900</pubDate>
    </item>
    <item>
      <title>[DB] 트랜잭션과 데이터베이스 락(Transction &amp;amp; DB Lock) | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/109</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;서론 (개인적인 생각)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;대부분 웹프로그래밍 업무는 CRUD가 기본 프로세스, 그 외에 전자결재 API, 외부 연계(인터페이스), 내부 연계, 기관 연계 등등 개발의 중점보다는(기본은 되는 정도) 업무(도메인)의 중점(중요도)이 더 크다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;예시로,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;신청과 관리- 내부에서도 승인(결재 프로세스)와 같은 구분이 있어야 visible 되는 기능 또는 화면도 있고,&amp;nbsp; 어떤 흐름에 따라 변경이 잦은 프로세스도 있을 것이다. 신청페이지와 관리자페이지의 상호작용이 중요하다고 생각이 들었다..&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;업무를 잘못 이해하면 개발을 아무리 잘해도 고객의 만족도를 높이지 못한다는 것.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 더 많은 테스트를 해보고, JUnit과 같이 코드로 진행하는 테스트로 먼저 검증하고 사용자 테스트를 거치거나 하는 식이다. (공공SI는 대부분 테스트코드 안 짜긴 함...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만, 그 전에 개발자가 테스트하고 [개발완료]를 찍을 것이다. 요구사항에 맞게 개발되었는지, 개발 중에도 테스트해보고 해당 프로세스의 흐름대로 잘 작동되나 기능을 테스트 할 것이다. 나는 개발 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;테스트 중 이상한 상황이 발생했다...&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. MERGE INTO 문을 화면에서 테스트하기 전에, 임의의 값을 넣고 DBeaver 툴에서 돌렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. INSERT / UPDATE 잘 되는 것을 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. 바로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;xml 매퍼에 적용해서 로직만들고, 빌드 후 로컬서버에서 돌려봄&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;4. (저장)버튼을 누르니 debug 걸린 것 마냥 서버가 멈췄음. (화면도 눌린 상태에서 먹통)&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 상태에 이해가 안 됐는데, 알고보니 트랜잭션이 메모리에만 반영되고, 실제 DB에 저장하지 않아서 트랜잭션 접근제한에 걸린 것이었다. (DBeaver Auto Commit 끈 상태 + Commit 안 날려줌)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이유&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;189&quot; data-start=&quot;89&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DBeaver에서 INSERT 실행&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 트랜잭션이 자동 커밋되지 않는 상태: 데이터가 메모리에만 반영되고, 아직 실제 DB에는 확정되지 않은 상태.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;392&quot; data-start=&quot;191&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;COMMIT 없이 테스트 진행&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 트랜잭션이 열린 상태에서 COMMIT 또는 ROLLBACK을 하지 않으면, 해당 레코드는 현재 세션에서만 보이고 다른 세션에서는 접근이 제한&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; DBeaver 세션이 아직 트랜잭션을 종료하지 않았기 때문에, 해당 레코드는 &lt;b&gt;락(Lock)에 의해 점유된 상태&lt;/b&gt;가 됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;558&quot; data-start=&quot;394&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;애플리케이션에서 매퍼를 통해 같은 테이블에 접근&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 애플리케이션의 커넥션 풀(Connection Pool)에서 새로운 DB 커넥션을 생성해 같은 테이블의 데이터를 읽거나 변경하려고 하면, &lt;b&gt;락이 걸려서 대기 상태(Blocking)로 멈추는 현상&lt;/b&gt;이 발생할 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;722&quot; data-start=&quot;560&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DB Lock 문제 해결 (Commit/Rollback 실행 후 정상 작동)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; DBeaver에서 COMMIT 또는 ROLLBACK을 수행하면 &lt;b&gt;트랜잭션이 종료되면서 락이 해제&lt;/b&gt;됨.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;rarr; 이후 애플리케이션에서 다시 실행하면 정상적으로 동작하게 됨.&lt;/span&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;span style=&quot;letter-spacing: 0px; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;나에게 발생한 DB Lock의 내용은 위와 같은 문제였다.. 그래서 알아봤다. DB Lock이 뭐고 왜 일어나는지?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-end=&quot;754&quot; data-start=&quot;729&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;원인 분석 (DB Lock의 종류)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;824&quot; data-start=&quot;784&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1) 테이블 레벨 락(Table-Level Lock)&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;952&quot; data-start=&quot;825&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;897&quot; data-start=&quot;825&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;INSERT, UPDATE, DELETE 후 &lt;b&gt;트랜잭션을 종료하지 않으면&lt;/b&gt;, 해당 테이블 전체가 잠길 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;952&quot; data-start=&quot;898&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다른 세션에서 &lt;b&gt;해당 테이블을 조회하거나 수정하려고 하면 대기 상태(Block)로 멈춤&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;985&quot; data-start=&quot;954&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2) 행(Row-Level Lock)&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1158&quot; data-start=&quot;986&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1069&quot; data-start=&quot;986&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;특정 행에 대한 UPDATE 또는 SELECT ... FOR UPDATE 수행 시, 해당 행이 잠겨 다른 트랜잭션이 접근하지 못하는 상태.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1158&quot; data-start=&quot;1070&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MySQL InnoDB 같은 경우 **MVCC(멀티 버전 동시성 제어)**로 인해 SELECT는 영향을 덜 받지만, UPDATE는 블록될 수 있음.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1225&quot; data-start=&quot;1160&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3) 트랜잭션이 끝나지 않아 발생하는 잠금 (Uncommitted Transaction Lock)&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1355&quot; data-start=&quot;1226&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1295&quot; data-start=&quot;1226&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DBeaver에서 INSERT 후 COMMIT을 하지 않으면, &lt;b&gt;다른 세션이 해당 데이터에 접근할 수 없음&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1355&quot; data-start=&quot;1296&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애플리케이션에서 동일한 데이터에 접근할 때 &lt;b&gt;락이 걸려서 응답이 멈춘 것처럼 보이는 현상&lt;/b&gt;이 발생.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 해결 방법&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-end=&quot;1405&quot; data-start=&quot;1374&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;1) DBeaver 트랜잭션 설정 확인&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1536&quot; data-start=&quot;1406&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1468&quot; data-start=&quot;1406&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DBeaver에서 Auto Commit 옵션이 꺼져 있으면, SQL 실행 후에도 트랜잭션이 계속 유지됨.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1536&quot; data-start=&quot;1469&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; Auto Commit을 활성화하거나, 명시적으로 COMMIT 또는 ROLLBACK을 수행.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;h4 data-end=&quot;1574&quot; data-start=&quot;1538&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Auto Commit 설정 방법 (DBeaver)&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;1705&quot; data-start=&quot;1575&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;1636&quot; data-start=&quot;1575&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DBeaver에서 Database &amp;rarr; Transaction &amp;rarr; Auto Commit을 활성화.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1705&quot; data-start=&quot;1637&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또는 COMMIT을 수동으로 실행 (CTRL + ENTER로 실행하지 말고 COMMIT; 입력 후 실행).&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(개인프로젝트 아니면 추천 안 함.)&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;1732&quot; data-start=&quot;1707&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1732&quot; data-start=&quot;1707&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2) 트랜잭션을 명확히 관리&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1843&quot; data-start=&quot;1733&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1797&quot; data-start=&quot;1733&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애플리케이션에서 &lt;b&gt;가능한 한 빠르게 트랜잭션을 종료&lt;/b&gt;(즉, COMMIT 또는 ROLLBACK 수행).&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;1843&quot; data-start=&quot;1798&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;트랜잭션이 필요한 경우, 명확한 로직 안에서만 유지하고 필요할 때 즉시 종료.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1874&quot; data-start=&quot;1845&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1874&quot; data-start=&quot;1845&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3) 락이 걸렸을 때 확인하는 방법&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-end=&quot;1898&quot; data-start=&quot;1875&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;ex) DB: MySQL&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1898&quot; data-start=&quot;1875&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3.1. 현재 락 확인&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741875380131&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM information_schema.innodb_trx;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-end=&quot;1982&quot; data-start=&quot;1955&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-end=&quot;1982&quot; data-start=&quot;1955&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3.2. 특정 세션 강제 종료 (MySQL)&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1741875427110&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;KILL [SESSION_ID];&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.25em; letter-spacing: -1px; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3.3. PostgreSQL에서 락 확인&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1741875444067&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT * FROM pg_stat_activity WHERE state = 'active';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-end=&quot;2132&quot; data-start=&quot;2107&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-end=&quot;2132&quot; data-start=&quot;2107&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3.4. 락 해제 (PostgreSQL)&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre id=&quot;code_1741875456732&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT pg_terminate_backend([PID]);&lt;/code&gt;&lt;/pre&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;h2 data-end=&quot;2194&quot; data-start=&quot;2186&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;결론&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2426&quot; data-start=&quot;2195&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2271&quot; data-start=&quot;2195&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DBeaver에서 트랜잭션이 열린 상태에서 COMMIT을 하지 않으면, 락이 걸려 애플리케이션에서 접근이 차단될 수 있음.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2345&quot; data-start=&quot;2272&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Auto Commit 설정을 확인하고, &lt;b&gt;테스트 후에는 반드시 COMMIT 또는 ROLLBACK을 수행해야 함&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;2426&quot; data-start=&quot;2346&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;DB 락이 걸렸을 때는 information_schema나 pg_stat_activity 등을 활용해 원인을 찾아볼 수 있음&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2494&quot; data-start=&quot;2428&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제부터는 &lt;b&gt;테스트 후에는 항상 COMMIT 또는 ROLLBACK을 습관적으로 수행해야겠다...&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2494&quot; data-start=&quot;2428&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2494&quot; data-start=&quot;2428&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;2494&quot; data-start=&quot;2428&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>실무</category>
      <category>AUTOCOMMIT</category>
      <category>COMMIT</category>
      <category>DBeaver</category>
      <category>dblock</category>
      <category>deadlock</category>
      <category>MySQL</category>
      <category>PostgreSQL</category>
      <category>Rollback</category>
      <category>데이터베이스</category>
      <category>트랜잭션</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/109</guid>
      <comments>https://2minmin2.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 13 Mar 2025 23:21:07 +0900</pubDate>
    </item>
    <item>
      <title>[DB] ROWNUMBER와 RANK의 차이 | 민민의 하드디스크 - 티스토리</title>
      <link>https://2minmin2.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 중 UI팝업을 누르면 사용자가 최근 사용한 주소 10개 데이터를 그리드로 출력해야 되는 화면이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트 쪽에서는 사실 그리드에 데이터만 뿌려주면 되고, 적용버튼(or 더블클릭이벤트)이 눌리면 선택된 주소가 자동으로 텍스트박스에 바인드 되는 정도의 기능만 있으면 돼서, 쿼리만 잘 짜주면 되겠다 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;내가 생각했던 쿼리는 최근 사용 주소 '10개' 니까 사용자id에 따라 RANK로 묶어서 RN &amp;lt;= 10 이렇게 두면 해당 사용자에 대한 최근 주소데이터 10개를 조회한다고 생각했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 예제 데이터가 있다고 가정해보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;예제 데이터&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;1030&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GLLJ6/btsMEdsCfwW/Z7gACNZgDlKsLqYB7P0ASK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GLLJ6/btsMEdsCfwW/Z7gACNZgDlKsLqYB7P0ASK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GLLJ6/btsMEdsCfwW/Z7gACNZgDlKsLqYB7P0ASK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGLLJ6%2FbtsMEdsCfwW%2FZ7gACNZgDlKsLqYB7P0ASK%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;1512&quot; height=&quot;1030&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;1030&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;원하는 출력 결과&lt;/b&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SuZp9/btsMDRQKK6u/f1R1FOMADZwChQxeeBHmBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SuZp9/btsMDRQKK6u/f1R1FOMADZwChQxeeBHmBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SuZp9/btsMDRQKK6u/f1R1FOMADZwChQxeeBHmBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSuZp9%2FbtsMDRQKK6u%2Ff1R1FOMADZwChQxeeBHmBK%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;1502&quot; height=&quot;748&quot; data-origin-width=&quot;1502&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. RANK() 사용 (잘못된 방식)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1741507655176&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM (
    SELECT *,
           RANK() OVER (PARTITION BY user_id ORDER BY used_at DESC) AS rn
    FROM recent_addresses
    WHERE user_id = 101
) ranked
WHERE rn &amp;lt;= 10;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 짜면 최근 10개의 사용주소가 나올 줄 알았는데 , 14rows의 결과가 나왔다. 알고 보니, rank는 중복이 가능했고 1, 2, 3, 4, ... 순차적으로 쌓이는 것이 아니라, 만약 기준 데이터가 동일하다면? 1, 2, 2, 3, 4, .. 이런식으로 중복되는 rank도 집계가 되는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 rank &amp;lt;= 10 조건으로 두면 1, 2, 2, 3, 4, 5, 5, 5, 6, ... 이런 식으로 중복되는 모든 숫자들을 보여주기에 10개가 넘을 수도 있다.&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. ROW_NUMBER() 사용 (올바른 방식)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1741508035515&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT *
FROM (
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY used_at DESC) AS rn
    FROM recent_addresses
    WHERE user_id = 101
) numbered
WHERE rn &amp;lt;= 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 ROW_NUMBER()로 두면, 순번이 순차적으로 중복없이 쌓이기 때문에 10개의 결과데이터가 나온다.&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;3395&quot; data-start=&quot;3352&quot;&gt;ROW_NUMBER()를 사용하여 중복되지 않는 연속적인 번호를 부여.&lt;/li&gt;
&lt;li data-end=&quot;3447&quot; data-start=&quot;3396&quot;&gt;WHERE rn &amp;lt;= 10 조건을 사용하여 정확히 10개의 최근 사용 주소를 가져옴.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 user_id의 최근 사용 주소 10개를 가져올 때는 RANK()가 아니라 ROW_NUMBER()를 사용해야 정확한 개수를 유지할 수 있다.&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;&lt;b&gt;최종 결론&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 55px;&quot; border=&quot;1&quot; data-end=&quot;3715&quot; data-start=&quot;3468&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody data-end=&quot;3715&quot; data-start=&quot;3569&quot;&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;함수&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;중복처리방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;건너뛰는 순위 여부&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;사용 경우 추천&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-end=&quot;3648&quot; data-start=&quot;3569&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;ROW_NUMBER()&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;각 행에 고유한 번호 부여&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;❌ 없음 (연속된 번호)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;정확히 N개의 데이터를 가져와야 할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot; data-end=&quot;3715&quot; data-start=&quot;3649&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;RANK()&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;동일 값이면 같은 순위 부여&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>실무</category>
      <category>db 랭크 로우넘버 차이</category>
      <category>rank row_number 차이</category>
      <category>rank()</category>
      <category>ROWNUM</category>
      <category>ROWNUMBER</category>
      <category>ROW_NUMBER</category>
      <category>ROW_NUMBER()</category>
      <category>최근 사용 주소 쿼리</category>
      <category>쿼리 rank</category>
      <author>민민2</author>
      <guid isPermaLink="true">https://2minmin2.tistory.com/108</guid>
      <comments>https://2minmin2.tistory.com/108#entry108comment</comments>
      <pubDate>Sun, 9 Mar 2025 17:18:53 +0900</pubDate>
    </item>
  </channel>
</rss>