<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Dev.Hammy.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 08 Mar 2026 15:05:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Dev.Hammy.log</title>
            <url>https://velog.velcdn.com/images/dev_hammy/profile/3f9cc098-689b-40dd-a12c-82aaf5809a31/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Dev.Hammy.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_hammy" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Ubuntu] SAMSUNG Galaxy Book 4 Pro 360에 Ubuntu 24.04 설치 시 webcam 안되는 문제 해결]]></title>
            <link>https://velog.io/@dev_hammy/ubuntu-samsung-webcam-fix</link>
            <guid>https://velog.io/@dev_hammy/ubuntu-samsung-webcam-fix</guid>
            <pubDate>Sun, 08 Mar 2026 15:05:45 GMT</pubDate>
            <description><![CDATA[<h2 id="0-빠른-결론">0. 빠른 결론</h2>
<p>아래 본문의 6단계 과정부터 수행하시면 됩니다.</p>
<h2 id="1-최초의-인식">1. 최초의 인식</h2>
<h3 id="1-1-1단계-정보">1-1. 1단계 정보</h3>
<ul>
<li>Laptop Model Name : NT960QGK</li>
<li>OS : Ubuntu 24.04</li>
</ul>
<h4 id="확인된-증상-1---discord에서-ipu6라는-장치가-인식되지만-2011-error가-발생">확인된 증상 (1) - discord에서 &#39;ipu6&#39;라는 장치가 인식되지만 &#39;2011 error&#39;가 발생</h4>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/b7700f42-e1df-419e-a899-658700607fc9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/ceb7b2a3-b40a-4f00-b3b0-fcbdc468092b/image.png" alt=""></p>
<h4 id="확인된-증상-2---obs-studio에서-비디오-캡쳐-장치가-pipewire-v4l2로-확인되지만-화면이-나오지-않음">확인된 증상 (2) - obs studio에서 비디오 캡쳐 장치가 &#39;PipeWire&#39;, &#39;V4L2&#39;로 확인되지만 화면이 나오지 않음</h4>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/374d4ae1-74eb-4d19-8631-cae71d1fce90/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/39c7425c-f302-43b8-965b-902442921347/image.png" alt=""></p>
<p>비디오 캡처 장치 중 V4L2를 선택했을 시 장치는 &#39;ipu6&#39;, 입력은 &#39;Intel IPU6 ISYS Capture 47&#39;로 표시 되고 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/2ac08a22-2a9d-4da5-9409-b5d485ce797f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/ddee0078-5619-4aff-bcea-ab2f7ce7905b/image.png" alt=""></p>
<p>비디오 캡처 장치 중 PipeWire를 선택했을 시에는 카메라 접근 권한 요청 팝업이 뜬다. 권한 허용 했을 시 장치는 &#39;ipu6(V4L2)&#39; 라고 표시된다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>IPU6</strong>
IPU6(Intel Image Processing Unit 6)는 최신 Intel 플랫폼에서 사용되는 카메라 전용 이미지 처리 하드웨어 블록이다. 주로 MIPI CSI 방식의 내장 노트북 카메라를 처리하며, 기존의 단순 UVC USB 카메라와 달리 ISP(Image Signal Processor) 파이프라인을 통해 노이즈 제거, 색 보정, 자동 노출 등 복잡한 영상 처리를 수행한다. Ubuntu에서 IPU6 기반 카메라는 일반 UVC 드라이버로는 동작하지 않으며, 전용 커널 드라이버와 사용자 공간 라이브러리가 필요하다.</li>
</ul>
<ul>
<li><strong>PipeWire</strong>
PipeWire 는 Linux에서 오디오와 비디오 스트림을 통합 관리하는 멀티미디어 서버이다. 기존의 PulseAudio(오디오)와 일부 JACK 기능을 대체하며, 최근 Ubuntu에서는 기본 오디오/영상 세션 매니저로 사용된다. 웹캠의 경우 V4L2에서 전달된 비디오 스트림을 사용자 애플리케이션(예: 브라우저, Zoom, Cheese)으로 연결해 주는 역할을 하며, 권한 관리 및 스트림 라우팅을 담당한다.</li>
</ul>
<ul>
<li><strong>V4L2</strong>
Video4Linux2 는 Linux 커널의 비디오 장치 인터페이스 표준이다. 웹캠, TV 튜너, 캡처 카드 등의 장치를 <code>/dev/video*</code> 형태의 디바이스 노드로 노출하며, 사용자 공간 프로그램이 이를 통해 영상 데이터를 읽고 제어할 수 있게 한다. 일반 USB UVC 카메라는 V4L2 드라이버(예: uvcvideo)로 바로 동작하지만, IPU6 기반 카메라는 내부적으로 ISYS/PSYS 레이어를 거쳐 V4L2 인터페이스로 연결된다.</li>
</ul>
<ul>
<li><strong>Intel IPU6 ISYS Capture 47</strong>
“Intel IPU6 ISYS Capture 47”은 IPU6의 ISYS(Input System) 파이프라인을 통해 노출되는 V4L2 캡처 디바이스 이름 중 하나이다. ISYS는 카메라 센서에서 들어오는 원시(raw) 데이터를 수집하는 단계이며, 이후 PSYS(Processing System)에서 영상 처리 과정을 수행한다. <code>v4l2-ctl --list-devices</code> 실행 시 해당 이름이 보인다면, 커널 차원에서 IPU6 입력 장치는 인식되었음을 의미하지만, 실제 영상 출력이 가능하려면 추가 사용자 공간 드라이버 및 미디어 스택 구성이 필요하다.</li>
</ul>
<ul>
<li><strong>MIPI CSI</strong>
MIPI CSI(Camera Serial Interface)는 모바일 및 노트북에서 널리 사용되는 고속 직렬 카메라 인터페이스 표준이다. 센서에서 SoC로 원시 영상 데이터를 직접 전송하며, USB처럼 자체 비디오 프로토콜을 포함하지 않는다. 따라서 반드시 ISP나 전용 이미지 처리 장치(IPU 등)를 거쳐야 화면 출력이 가능하다.</li>
</ul>
<ul>
<li><strong>UVC USB 카메라</strong>
UVC(USB Video Class)는 USB 표준에 포함된 비디오 장치 규격이다. UVC 카메라는 자체적으로 비디오 스트림 포맷을 제공하므로, Linux에서는 <code>uvcvideo</code> 드라이버만으로 바로 <code>/dev/videoX</code> 장치가 생성된다. 별도의 ISP 파이프라인 없이도 비교적 단순하게 동작한다는 점에서 MIPI CSI 카메라와 구조적으로 다르다.</li>
</ul>
<ul>
<li><strong>커널 드라이버</strong>
커널 드라이버는 운영체제 커널 공간에서 하드웨어를 제어하는 소프트웨어 모듈이다. IPU6의 경우 센서 제어, DMA 처리, 인터럽트 관리 등을 수행하며, V4L2 인터페이스로 사용자 공간에 장치를 노출한다. 드라이버가 로드되지 않으면 장치는 물리적으로 존재하더라도 <code>/dev/videoX</code>가 생성되지 않는다.</li>
</ul>
<ul>
<li><strong>JACK 기능</strong>
JACK(Jack Audio Connection Kit)은 저지연 오디오 처리를 위한 Linux 오디오 서버이다. 전문 음악 제작 환경에서 오디오 애플리케이션 간 실시간 라우팅을 지원한다. PipeWire는 JACK의 저지연 처리 기능과 연결 모델을 부분적으로 흡수하여, 오디오 및 비디오 스트림을 통합적으로 관리할 수 있도록 설계되었다.</li>
</ul>
</blockquote>
<h3 id="1-2-1단계에서-유추-가능한-원인">1-2. 1단계에서 유추 가능한 원인</h3>
<p>위 증상을 해석하기 위해서는 IPU6 기반 카메라가 Ubuntu 24.04에서 어떤 경로로 동작하는지 먼저 이해할 필요가 있다.</p>
<h4 id="ipu6--v4l2--pipewire-데이터-흐름-구조">IPU6 + V4L2 + PipeWire 데이터 흐름 구조</h4>
<p>IPU6 기반 내장 카메라는 일반 USB 웹캠과 구조가 다르다. 전체 흐름은 다음과 같다.</p>
<p><strong>1단계 — 이미지 센서 입력</strong>
노트북 내부 카메라는 일반적으로 MIPI CSI 인터페이스를 통해 SoC로 영상 신호를 전달한다. 이 신호는 USB처럼 완성된 비디오 스트림이 아니라, 원시(raw) 센서 데이터 형태로 들어온다.</p>
<p><strong>2단계 — IPU6 ISYS 처리</strong>
Intel IPU6의 ISYS(Input System)는 센서로부터 들어온 MIPI CSI 신호를 수집하고 프레임 단위로 구성한다. 이 단계에서 물리적 링크 관리와 DMA 전송이 수행된다.</p>
<p><strong>3단계 — IPU6 PSYS 영상 처리</strong>
PSYS(Processing System)는 ISP 파이프라인을 통해 자동 노출(AE), 자동 화이트밸런스(AWB), 노이즈 제거, 색 보정 등을 수행한다. 이 과정을 거쳐 사람이 볼 수 있는 YUV/RGB 프레임으로 변환된다.</p>
<p><strong>4단계 — 커널 공간에서 V4L2 디바이스 노출</strong>
처리된 영상 스트림은 Video4Linux2 인터페이스를 통해 <code>/dev/videoX</code> 형태의 장치 노드로 사용자 공간에 노출된다. 이 시점에서 시스템은 이를 “웹캠 장치”로 인식한다.</p>
<p><strong>5단계 — 사용자 공간 미디어 서버 연결</strong>
Ubuntu 24.04에서는 PipeWire가 기본 미디어 서버로 동작한다. PipeWire는 V4L2 디바이스를 감지하고 이를 내부 스트림 그래프에 연결한다.</p>
<p><strong>6단계 — 애플리케이션 전달</strong>
Discord, OBS Studio, 브라우저 등은 PipeWire를 통해 영상 스트림을 수신한다. 이 단계에서 권한 확인, 세션 관리, 스트림 라우팅이 처리된다.</p>
<p>요약 구조는 다음과 같다.</p>
<p><strong>MIPI 센서 → IPU6(ISYS → PSYS) → V4L2(<code>/dev/videoX</code>) → PipeWire → 애플리케이션</strong> </p>
<h4 id="1-가설-a--커널-및-미디어-그래프ipu6-→-v4l2-단계에서의-문제">(1) 가설 A : 커널 및 미디어 그래프(IPU6 → V4L2) 단계에서의 문제</h4>
<p>OBS에서 장치를 “Intel IPU6 ISYS Capture 47”로 인식한다는 것은 최소한 V4L2 장치 노출까지는 이루어졌음을 의미한다. 즉, IPU6 ISYS 드라이버가 로드되어 <code>/dev/videoX</code>가 생성된 상태이다.</p>
<p>그러나 화면이 출력되지 않는다는 것은, 커널 및 미디어 파이프라인 단계에서 실제 프레임 생성이 이루어지지 않았을 가능성을 시사한다. 가능한 원인은 다음과 같다.</p>
<ul>
<li>PSYS(Processing System) 파이프라인이 정상적으로 동작하지 않음</li>
<li>미디어 컨트롤러(Media Controller) 그래프 구성이 불완전함</li>
<li>V4L2 레벨에서의 포맷 협상 실패 (픽셀 포맷, 해상도, 버퍼 설정 문제)</li>
</ul>
<p>이 경우는 “디바이스 노드는 존재하지만, 커널 내부에서 프레임이 생성되지 않는 상태”에 해당한다. 즉, <code>/dev/videoX</code>는 생성되었으나 실제 영상 데이터가 사용자 공간으로 전달되지 않는 구조적 문제이다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>포맷 협상 (Format Negotiation)</strong>
포맷 협상은 카메라 장치와 애플리케이션 사이에서 어떤 해상도, 픽셀 포맷(YUYV, NV12, RGB 등), 프레임레이트를 사용할지 결정하는 과정이다. V4L2 장치는 여러 포맷을 지원할 수 있으며, 애플리케이션은 자신이 처리 가능한 포맷을 요청한다. 이때 양측이 공통으로 지원하는 포맷이 합의되어야 영상이 정상적으로 출력된다. IPU6 환경에서는 ISP 출력 포맷과 PipeWire 또는 애플리케이션이 요구하는 포맷이 일치하지 않으면 협상에 실패할 수 있으며, 그 결과 장치는 보이지만 화면은 검게 표시되는 현상이 발생할 수 있다.</li>
</ul>
<ul>
<li><strong>미디어 컨트롤러 (Media Controller)</strong>
미디어 컨트롤러는 Linux 커널의 V4L2 하위 프레임워크로, 복잡한 카메라 파이프라인 구조를 그래프 형태로 관리하는 시스템이다. 단순 UVC USB 카메라는 하나의 장치 노드로 끝나지만, IPU6와 같은 MIPI 기반 카메라는 센서, ISYS, PSYS, ISP, 메모리 노드 등 여러 하드웨어 블록이 연결된 구조를 가진다. 미디어 컨트롤러는 이러한 구성 요소들을 엔티티(entity)와 링크(link)로 표현하며, 올바른 연결이 설정되어야 프레임이 생성된다. 설정이 불완전하면 <code>/dev/videoX</code>는 존재하더라도 실제 영상 데이터가 흐르지 않을 수 있다.</li>
</ul>
</blockquote>
<h4 id="2-가설-b--ipu6-사용자-공간-스택-미완성-문제">(2) 가설 B : IPU6 사용자 공간 스택 미완성 문제</h4>
<p>IPU6 기반 카메라는 단순 UVC USB 카메라와 달리, 커널 드라이버만으로 완전 동작하지 않는다. 커널에서 V4L2 장치를 노출하더라도, 사용자 공간에서 ISP 파이프라인을 구성하는 추가 구성요소가 필요하다. 이 계층에서 발생할 수 있는 문제는 다음과 같다.</p>
<ul>
<li>IPU6 관련 사용자 공간 라이브러리(libcamera, ipu6 userspace 패키지 등) 누락</li>
<li>ISP 제어 서비스 미기동</li>
<li>사용자 공간에서의 파이프라인 초기화 실패</li>
<li>커널 ↔ 사용자 공간 인터페이스 연동 오류</li>
</ul>
<p>이 경우는 “커널 단계는 통과했으나, 사용자 공간 영상 처리 체인이 완성되지 않은 상태”에 해당한다. 즉, <code>/dev/videoX</code>는 존재하고 장치도 인식되지만, ISP 제어 및 프레임 처리 로직이 정상적으로 구성되지 않아 실제 영상 출력이 이루어지지 않는다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>사용자 공간 (User Space)</strong>
사용자 공간은 운영체제에서 일반 애플리케이션과 라이브러리가 실행되는 영역을 의미한다. Linux는 크게 커널 공간(kernel space)과 사용자 공간(user space)으로 구분되며, 하드웨어 직접 제어는 커널이 담당하고, 실제 프로그램 실행은 사용자 공간에서 이루어진다. IPU6 맥락에서 커널 드라이버는 <code>/dev/videoX</code> 장치를 생성하지만, libcamera나 PipeWire 같은 구성 요소는 사용자 공간에서 동작하며 영상 스트림을 처리하고 애플리케이션에 전달한다. 따라서 “사용자 공간 라이브러리 누락”이라는 것은 커널은 정상이나, 그 위에서 동작해야 할 영상 처리 스택이 완성되지 않았음을 의미한다.</li>
</ul>
<ul>
<li><strong>ISP(Image Signal Processor) 제어</strong>
ISP 제어는 이미지 센서에서 들어온 원시 데이터를 사람이 볼 수 있는 영상으로 변환하기 위해 수행되는 처리 파라미터를 설정·조정하는 과정을 의미한다. ISP는 자동 노출(AE), 자동 화이트밸런스(AWB), 색 보정, 노이즈 제거, 감마 보정 등의 연산을 수행하는데, 이 값들은 고정되어 있지 않고 센서 상태와 조명 환경에 따라 동적으로 조절된다. IPU6 환경에서는 커널 드라이버와 사용자 공간 라이브러리가 협력하여 ISP 블록에 필요한 설정을 전달한다. 이 제어가 이루어지지 않으면 센서는 동작하더라도 영상은 생성되지 않거나, 생성되더라도 정상적인 프레임으로 출력되지 않을 수 있다.</li>
</ul>
</blockquote>
<h4 id="3-가설-c--pipewire-레벨에서의-스트림-연결-문제">(3) 가설 C : PipeWire 레벨에서의 스트림 연결 문제</h4>
<p>PipeWire를 선택했을 때 카메라 접근 권한 팝업이 뜨는 것은, V4L2 장치를 PipeWire가 감지하고 있음을 의미한다.</p>
<p>그러나:</p>
<ul>
<li>Discord에서 2011 error 발생</li>
<li>PipeWire 선택 시 화면 미출력</li>
</ul>
<p>이 경우는 다음을 의심할 수 있다.</p>
<ul>
<li>PipeWire 세션 매니저(wireplumber) 구성 문제</li>
<li>포털(xdg-desktop-portal) 권한 연동 오류</li>
<li>PipeWire ↔ V4L2 노드 연결 실패</li>
<li>스트림 negotiation 실패</li>
</ul>
<p>즉, 커널 단계는 통과했으나 사용자 공간 미디어 스택에서 실패했을 가능성이다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>세션 매니저 (Session Manager)</strong>
세션 매니저는 PipeWire 내부에서 오디오·비디오 장치와 애플리케이션 간의 연결 정책을 관리하는 구성 요소이다. Ubuntu 24.04에서는 기본적으로 wireplumber가 세션 매니저로 동작하며, 어떤 장치를 어떤 애플리케이션에 연결할지, 권한은 어떻게 처리할지, 기본 장치는 무엇으로 설정할지 등을 결정한다. 단순히 장치가 존재한다고 해서 자동으로 스트림이 연결되는 것이 아니라, 세션 매니저의 정책에 따라 실제 링크가 생성된다. 따라서 wireplumber 설정 오류나 비정상 상태가 발생하면 장치는 보이지만 영상이 전달되지 않을 수 있다.</li>
</ul>
<ul>
<li><strong>포털 (xdg-desktop-portal)</strong>
포털은 샌드박스 환경이나 보안 모델 하에서 애플리케이션이 시스템 자원에 접근할 수 있도록 중개하는 인터페이스 계층이다. Ubuntu에서는 xdg-desktop-portal이 카메라, 마이크, 화면 공유 등의 접근 요청을 처리하며, 사용자가 허용 여부를 선택할 수 있도록 팝업을 생성한다. PipeWire와 연동하여 카메라 접근 권한을 승인받은 후에만 스트림 연결이 이루어진다. 포털 연동에 문제가 생기면 권한 팝업은 뜨지만 실제 스트림이 애플리케이션으로 전달되지 않는 상황이 발생할 수 있다.</li>
</ul>
<ul>
<li><strong>노드 연결 (Node Linking)</strong>
PipeWire는 오디오·비디오 구성 요소를 “노드(node)” 단위로 표현한다. 예를 들어 V4L2 카메라 장치도 하나의 노드이며, Discord나 OBS 같은 애플리케이션도 입력을 소비하는 노드로 표현된다. 노드 연결은 이들 사이에 실제 데이터 경로를 생성하는 과정을 의미한다. 연결이 성공해야 카메라 프레임이 애플리케이션으로 전달된다. 노드가 존재하더라도 링크(link)가 생성되지 않으면 스트림은 흐르지 않는다.</li>
</ul>
<ul>
<li><strong>스트림 negotiation (Stream Negotiation)</strong>
스트림 negotiation은 PipeWire 내부에서 두 노드가 데이터 포맷, 해상도, 프레임레이트 등을 합의하는 과정이다. 이는 V4L2 단계의 포맷 협상과 유사하지만, PipeWire 그래프 내에서 다시 한 번 수행된다. 예를 들어 카메라가 NV12 포맷을 출력하고 애플리케이션이 RGB만 처리 가능하다면, 변환 노드가 삽입되거나 협상이 실패할 수 있다. 이 협상이 실패하면 장치는 정상 인식되지만 영상이 출력되지 않거나 애플리케이션에서 오류 코드가 발생할 수 있다.</li>
</ul>
</blockquote>
<h2 id="2-간단한-명령어로-상태-확인">2. 간단한 명령어로 상태 확인</h2>
<p>이 단계의 목표는 “장치가 보인다” 수준을 넘어서, <strong>프레임이 실제로 생성되는지</strong>와 <strong>어느 계층에서 끊기는지</strong>를 분리하는 것이다. IPU6 문제는 대체로 다음 3계층 중 하나에서 발생한다.</p>
<ul>
<li>커널/미디어 그래프(IPU6 → V4L2) → 가설 A</li>
<li>IPU6 사용자 공간 스택(libcamera 등) → 가설 B</li>
<li>PipeWire/포털/세션 연결 → 가설 C</li>
</ul>
<h3 id="2-1-커널-및-플랫폼-정보-확인">2-1. 커널 및 플랫폼 정보 확인</h3>
<pre><code class="language-bash">uname -r
cat /proc/cpuinfo | grep &quot;model name&quot; | head -1</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/02fc5f31-a781-4c72-a8c7-e3526b5db0a2/image.png" alt=""></p>
<p>이 정보는 커널이 IPU6 드라이버와 관련 패치를 포함하고 있을 가능성을 가늠하는 “환경 컨텍스트”다. 다만 여기서 바로 지원 여부를 단정할 수 없으므로, 다음 단계에서 실제 장치 노출과 파이프라인 상태를 확인한다.</p>
<h3 id="2-2-v4l2-장치-노출-상태-확인-abc-공통-전제">2-2. V4L2 장치 노출 상태 확인 (A/B/C 공통 전제)</h3>
<pre><code class="language-bash">sudo apt install -y v4l-utils
v4l2-ctl --list-devices</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/4fe67caf-d708-47a4-837d-e24c7da9a385/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c444e58d-7693-49bf-86ff-43355e214858/image.png" alt=""></p>
<p>수많은 <code>/dev/videoX</code> 노드가 보인다는 것은 IPU6 드라이버가 로드되어 V4L2 인터페이스를 생성했음을 의미한다. 그러나 이 출력만으로는 다음을 알 수 없다.</p>
<ul>
<li>어떤 노드가 실제 영상 스트림을 제공하는지</li>
<li>PSYS 파이프라인이 동작하는지</li>
<li>프레임이 실제로 생성되는지</li>
</ul>
<p>따라서 이 단계는 “장치 노출 확인”에 해당하며, 실제 영상 생성 여부는 다음 단계에서 별도로 검증해야 한다</p>
<h3 id="2-3-ipu6-isyspsys-커널-모듈-로딩-상태-확인-가설-a">2-3. IPU6 ISYS/PSYS 커널 모듈 로딩 상태 확인 (가설 A)</h3>
<pre><code class="language-bash">lsmod | grep -i ipu</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/381e0a3d-159a-4867-9b4d-47c19bf12da2/image.png" alt=""></p>
<p>출력 결과 <code>intel_ipu6</code> 및 <code>intel_ipu6_isys</code>는 로드되어 있었으나, <code>intel_ipu6_psys</code> 모듈은 확인되지 않았다. 이는 커널이 IPU6 입력 시스템(ISYS)을 통해 센서 신호를 수집할 준비는 되어 있으나, ISP 처리 파이프라인을 담당하는 PSYS가 커널 레벨에서 활성화되지 않았을 가능성을 시사한다. 결과적으로 <code>/dev/videoX</code> 노드가 다수 생성되더라도, 프레임을 YUV/RGB로 변환·출력하는 처리 단계가 구성되지 않아 “장치는 보이지만 화면은 나오지 않는” 증상으로 이어질 수 있다.</p>
<p>psys 모듈이 “안 보이는 이유”는 크게 3가지.</p>
<ul>
<li>커널에 psys가 빌드/패키징되어 있지 않음 (가장 흔함)</li>
<li>모듈은 있는데 로드가 실패함(펌웨어/의존성)</li>
<li>Ubuntu 쪽 패키지/드라이버 구조상 psys가 다른 이름으로 묶임 (가능성 낮음)</li>
</ul>
<p>그래서 다음 단계(2-4)에서는 “psys 모듈 파일 자체가 존재하는지”를 확인해보기로 한다.</p>
<h3 id="2-4-psys-모듈-파일-존재-여부-확인-가설-a">2-4. PSYS 모듈 파일 존재 여부 확인 (가설 A)</h3>
<pre><code class="language-bash">modinfo intel_ipu6_psys 2&gt;/dev/null | head</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/e2dd799c-8722-4ce9-a9d9-b9e558e162c4/image.png" alt=""></p>
<pre><code class="language-bash">grep -iE &#39;IPU6|PSYS|ISYS&#39; /boot/config-$(uname -r)</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/35dc996b-ee60-4ce1-acf5-f7d464def3e1/image.png" alt=""></p>
<pre><code class="language-bash">find /lib/modules/$(uname -r) -iname &#39;*ipu6*psys*.ko*&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/41a06876-f72d-4975-9b5b-e437a13ad0db/image.png" alt=""></p>
<p><code>ntel_ipu6_isys</code>모듈은 로드되어 있으나 <code>ntel_ipu6_psys</code>모듈은 확인되지 않았다. 또한 <code>odinfo intel_ipu6_psys</code> 정보를 반환하지 않았고, <code>lib/modules/$(uname -r)에서 *ipu6*psys*.ko*</code>파일도 발견되지 않았다. 이는 현재 커널 빌드(6.17.0-14-generic)에서 IPU6 PSYS 드라이버가 모듈 형태로 제공되지 않음을 의미한다. 결과적으로 입력(ISYS) 계층은 존재하지만, 영상 처리(PSYS/ISP) 계층이 누락되어 “장치는 보이나 프레임이 생성되지 않는” 증상으로 이어질 수 있다.</p>
<p>여기까지의 관찰만 놓고 보면 “PSYS 모듈이 없으니 PSYS만 설치하면 끝”처럼 보이지만, IPU6 카메라 스택은 그렇게 단순하지 않다. IPU6 기반 MIPI 카메라는 커널 드라이버(ISYS/PSYS)만으로 완전 동작하지 않고, 사용자 공간에서 libcamera 파이프라인(IPA 모듈, 펌웨어/튜닝 데이터)과 PipeWire 스트림 협상까지 연결되어야 애플리케이션에서 실제 프레임이 나온다. 따라서 다음 단계에서는 문제를 “PSYS 부재”로 단정하기 전에, 영상 파이프라인이 어느 계층에서 끊기는지(A/B/C)부터 다시 분리해 확인한다.</p>
<h3 id="2-5-libcamera에서-카메라-파이프라인이-열리는지-확인-가설-b-검증">2-5 libcamera에서 카메라 파이프라인이 열리는지 확인 (가설 B 검증)</h3>
<p>V4L2에서 <code>/dev/videoX</code>가 보인다고 해서 곧바로 “카메라가 동작한다”라고 말할 수는 없다. IPU6 계열은 특히 커널 드라이버(장치 노출)와 사용자 공간 파이프라인 구성(실제 영상 생성)이 분리되어 있는 경우가 많다.
즉, 커널 단계에서 ISYS까지 올라와 <code>/dev/videoX</code>가 생성되더라도, 사용자 공간에서 ISP 파이프라인(IPA 모듈, 튜닝 데이터, 펌웨어 등)이 제대로 잡히지 않으면 화면은 끝까지 나오지 않는다.</p>
<p>이때 가장 빠른 검증 도구가 <code>libcamera</code>다. <code>libcamera</code>는 단순히 V4L2 장치를 “열어보는” 수준이 아니라, 카메라 파이프라인을 구성하고 실제 프레임을 요청하는 흐름까지 수행한다. 따라서 <code>libcamera</code>가 카메라를 정상적으로 열 수 있는지는 “사용자 공간 스택이 완성됐는지(가설 B)”를 판단하는 강한 신호가 된다.</p>
<p>아래 순서로 확인한다.</p>
<pre><code class="language-bash">sudo apt install -y libcamera-tools
cam -l
cam -c 0</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/5dc9e3f4-49f3-497f-badb-44362a72de02/image.png" alt=""></p>
<p><code>cam -l</code> 결과에서 Available cameras가 비어 있거나, <code>No IPA found in &#39;/usr/lib/x86_64-linux-gnu/libcamera&#39;</code>와 같은 경고가 뜬다면 사용자 공간에서 IPA(파이프라인 알고리즘 플러그인)를 로딩하지 못해 파이프라인 초기화 자체가 불가능한 상태일 수 있다. 이 경우는 커널에서 장치 노드가 보이더라도 실제 영상 처리가 구성되지 않으므로 가설 B(사용자 공간 스택 미완성) 를 우선으로 본다.</p>
<p>반대로 <code>cam -c 0</code>에서 정상적으로 프레임이 나오는데 Discord/OBS에서만 실패한다면, 프레임 생성 자체는 성공한 것이므로 문제는 PipeWire/포털/세션 연결 또는 포맷 변환(가설 C) 로 좁혀진다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>IPA (Image Processing Algorithms)</strong>
IPA는 libcamera가 카메라 ISP를 제어하기 위해 사용하는 사용자 공간 알고리즘 모듈이다. IPU6 기반 카메라는 센서 데이터만으로는 영상이 생성되지 않으며, 자동 노출·화이트밸런스·색 보정 등의 처리 알고리즘이 적용되어야 정상적인 프레임이 만들어진다. libcamera는 해당 하드웨어에 맞는 IPA를 로드해 ISP 파이프라인을 초기화하고 제어한다. IPA가 없거나 로드되지 않으면 커널에서 장치가 보이더라도 libcamera 단계에서 파이프라인이 열리지 않으며, 결과적으로 영상이 출력되지 않는다.</li>
</ul>
</blockquote>
<h3 id="2-6-pipewire가-실제-스트림을-만들고-있는지-확인-가설-c-검증">2-6. PipeWire가 “실제 스트림”을 만들고 있는지 확인 (가설 C 검증)</h3>
<p>PipeWire 환경에서는 “카메라가 보인다”와 “카메라 스트림이 실제로 연결되어 흐른다”가 다르다.</p>
<p>권한 팝업이 뜨는 것은 포털(xdg-desktop-portal)이 장치 접근 요청을 받았고, PipeWire가 장치를 감지했다는 의미일 뿐이다. 그 다음 단계(노드 생성 → 링크 생성 → 포맷 협상)가 실패하면, 애플리케이션에서는 장치가 보이더라도 검은 화면 또는 오류 코드(예: Discord의 에러)로 끝난다.</p>
<p>PipeWire 쪽 상태는 서비스 상태 + 로그 두 축으로 확인한다.</p>
<pre><code class="language-bash">systemctl --user status pipewire wireplumber xdg-desktop-portal
journalctl --user -u pipewire -u wireplumber -u xdg-desktop-portal -e</code></pre>
<p>여기서 확인할 포인트는 다음과 같다.</p>
<ul>
<li>서비스가 죽어 있거나 반복 재시작 중인지 (pipewire / wireplumber / portal)</li>
<li>로그에 아래 류의 메시지가 있는지<ul>
<li>포맷/해상도 협상 실패(format negotiation failure)</li>
<li>노드 링크 실패(node link failed)</li>
<li>포털 권한/세션 처리 실패(portal permission/session failure)</li>
</ul>
</li>
</ul>
<p>이런 메시지가 확인되면, PipeWire-Portal-Session(=wireplumber) 레벨에서 스트림이 완성되지 못하는 문제, 즉 가설 C로 방향을 잡는 게 논리적으로 맞다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/2c8c9cf9-e1a5-4157-a0c1-549aab60f1a5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/dcd595c2-b391-4198-a44d-bdcb019a1f11/image.png" alt=""></p>
<pre><code>SPA handle &#39;api.libcamera.enum.manager&#39; could not be loaded; is it installed?
PipeWire&#39;s libcamera SPA missing or broken. libcamera not supported.</code></pre><p>이 메시지는 매우 직접적이다.</p>
<p>PipeWire가 libcamera 기반 카메라를 감지하려 했지만, libcamera SPA 플러그인을 로드하지 못했다는 뜻이다.</p>
<p>즉, PipeWire는 실행 중이지만 libcamera를 입력 소스로 사용할 수 없는 상태다. 이것은 포맷 협상 실패나 노드 링크 실패 같은 가설 C(미디어 세션/포털 문제)가 아니라, libcamera 계층 자체가 불완전하다는 가설 B(사용자 공간 스택 미완성)를 강하게 지지한다.</p>
<h3 id="2-7-dmesg로-부팅-당시-카메라-드라이버-초기화-로그-확인">2-7. dmesg로 부팅 당시 카메라 드라이버 초기화 로그 확인</h3>
<p>지금까지는 <code>ipu6</code>, <code>isys</code>, <code>psys</code>처럼 IPU6 계열 키워드를 중심으로 확인했다. 하지만 실제 부팅 로그에서는 문제가 꼭 <code>ipu6</code>라는 문자열로만 나타나지 않는다.</p>
<p>예를 들어 다음과 같은 경우가 있다.</p>
<ul>
<li>센서 드라이버(<code>ov02...</code>, <code>ov08...</code>, <code>imx...</code>, <code>gc...</code>) 초기화 실패</li>
<li>I2C 버스에서 센서 탐지 실패</li>
<li>ACPI에서 센서 정보 매칭 실패</li>
<li>펌웨어 로딩 실패</li>
<li>media graph 또는 subdev 등록 실패</li>
</ul>
<p>즉, <code>/dev/videoX</code>가 생성되어도 실제 원인은 IPU6 본체가 아니라 센서 드라이버나 주변 초기화 단계에 있을 수 있다.
따라서 <code>dmesg</code> 확인 단계에서는 특정 드라이버명만 찾지 말고, 카메라 스택 전반을 넓게 검색하는 것이 더 안전하다.</p>
<pre><code class="language-bash">dmesg | grep -iE &#39;ipu|isys|psys|camera|sensor|v4l2|media|uvc|mipi|csi|i2c|ov[0-9]+|imx[0-9]+|gc[0-9]+&#39;</code></pre>
<p>이 명령은 다음 범주의 로그를 한 번에 확인하기 위한 것이다.</p>
<ul>
<li>IPU6 본체: <code>ipu</code>, <code>isys</code>, <code>psys</code></li>
<li>카메라 일반 계층: <code>camera</code>, <code>sensor</code>, <code>v4l2</code>, <code>media</code></li>
<li>물리 인터페이스: <code>mipi</code>, <code>csi</code>, <code>i2c</code></li>
<li>센서 드라이버 계열: <code>ov...</code>, <code>imx...</code>, <code>gc...</code></li>
<li>USB 웹캠 가능성: <code>uvc</code></li>
</ul>
<p>특히 노트북 내장 카메라는 센서명이 직접 로그에 찍히는 경우가 많으므로, <code>ov...</code>, <code>imx...</code>, <code>gc...</code> 패턴까지 같이 보는 것이 좋다.</p>
<p>단순 키워드 검색만으로는 정상 로그와 오류 로그가 섞여서 보이기 때문에, 그다음에는 실패 패턴을 중심으로 한 번 더 필터링한다.</p>
<pre><code class="language-bash">dmesg | grep -iE &#39;ipu|isys|psys|camera|sensor|v4l2|media|uvc|mipi|csi|i2c|ov[0-9]+|imx[0-9]+|gc[0-9]+&#39; | grep -iE &#39;fail|error|warn|timeout|probe|unable|not found|no such|invalid|unsupported|cannot|can.t|firmware&#39;</code></pre>
<p>커널 로그는 실패를 여러 표현으로 남기므로, <code>fail</code>처럼 어근 단위로 잡는 편이 더 낫다. 예를 들어 다음과 같은 로그를 잡아낼 수 있다.</p>
<ul>
<li><code>probe failed</code></li>
<li><code>failed to register subdevice</code></li>
<li><code>error -22</code></li>
<li><code>firmware not found</code></li>
<li><code>unable to load</code></li>
<li><code>timeout waiting for response</code></li>
<li><code>unsupported pixel format</code></li>
</ul>
<p>배포판에 따라 <code>dmesg</code>보다 <code>journalctl -b -k</code>가 읽기 더 편한 경우도 있다.</p>
<pre><code class="language-bash">journalctl -b -k | grep -iE &#39;ipu|isys|psys|camera|sensor|v4l2|media|uvc|mipi|csi|i2c|ov[0-9]+|imx[0-9]+|gc[0-9]+&#39; | grep -iE &#39;fail|error|warn|timeout|probe|unable|not found|no such|invalid|unsupported|cannot|can.t|firmware&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/7cc8ce2b-8aeb-4a56-9584-fbfb52ed732a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/a3d692c1-ee34-4d0e-abaf-ca5cf9c2f2d7/image.png" alt=""></p>
<p>위 두 출력에서 가장 핵심적인 메시지는 아래와 같다.</p>
<pre><code>ov02c10 i2c-OVTI02C1:00: error -EINVAL: external clock 26000000 is not supported
ov02c10 i2c-OVTI02C1:00: probe with driver ov02c10 failed with error -22</code></pre><p>의미를 풀면 다음과 같다 :</p>
<ul>
<li>시스템이 카메라 센서를 <code>ov02c10</code>으로 인식하려고 시도했다.</li>
<li>센서 초기화 과정에서 외부 클럭 값 <code>26,000,000 Hz (26MHz)</code>를 받았는데, 현재 드라이버 또는 해당 구성에서는 이 클럭값을 지원하지 않는다고 판단했다.</li>
<li>그 결과 <code>probe</code>가 실패했다.</li>
<li><code>error -22</code>는 리눅스 커널에서 <code>EINVAL(Invalid argument)</code> 이다. 즉, 드라이버가 받은 파라미터나 하드웨어 설정값이 자신이 처리할 수 있는 범위와 맞지 않는다는 뜻이다.</li>
</ul>
<p>이건 단순한 경고가 아니라, 커널 레벨에서 센서 드라이버가 실제로 장치 바인딩에 실패했다는 뜻이다.</p>
<h3 id="2-8-중간-결론">2-8. 중간 결론</h3>
<p>원칙적으로는 여기서 다음을 더 확인할 수도 있다.</p>
<ul>
<li>해당 노트북 모델에 맞는 OEM 커널이 따로 존재하는지</li>
<li>Ubuntu 패키지 조합을 바꾸면 해결되는지</li>
<li>out-of-tree IPU6 드라이버 스택에 이미 수정이 반영되어 있는지</li>
</ul>
<p>하지만 이번 글에서는 이 탐색을 더 이어가지 않기로 한다.
이유는 이미 충분히 많은 간접 정황이 쌓였고, 무엇보다 커널 로그가 센서 probe 실패를 매우 직접적으로 가리키고 있기 때문이다. 즉, 더 많은 검색과 패키지 비교를 반복하기보다, 문제의 진원지로 보이는 센서 드라이버 코드 자체를 확인하고 수정하는 편이 오히려 더 빠르다고 판단했다.</p>
<p>따라서 현 단계의 중간 결론은 다음과 같다.</p>
<ul>
<li>1차 문제 지점은 PipeWire나 OBS가 아니라 커널 레벨의 센서 드라이버 초기화 단계로 본다.</li>
<li>현재 환경에서는 <code>ov02c10</code> 드라이버가 26MHz external clock 구성을 수용하지 못해 probe에 실패하고 있다.</li>
<li>이 문제를 우회하거나 해소하지 않으면, 상위 계층(IPU6 파이프라인, libcamera, PipeWire) 점검은 근본 해결로 이어지기 어렵다.</li>
<li>따라서 다음 단계에서는 검색이나 패키지 재설치보다, <code>ov02c10</code> 센서 드라이버 소스를 직접 열어 클럭 허용 조건과 probe 경로를 확인하는 방식으로 접근한다.</li>
</ul>
<p>즉, 이제부터의 문제 해결 방향은 “환경을 더 검색해보는 것”이 아니라, “센서 드라이버 코드가 왜 26MHz를 거부하는지 직접 확인하고 수정하는 것”이다.</p>
<h2 id="3-ov02c10-센서-드라이버-소스-수정-및-반영하기">3. <code>ov02c10</code> 센서 드라이버 소스 수정 및 반영하기</h2>
<h3 id="3-1-ov02c10-소스-코드-수정">3-1. <code>ov02c10</code> 소스 코드 수정</h3>
<p>그래서 다음 단계는 “ov02c10 드라이버 소스가 실제로 어디에 있고, 어떤 커널 패키지에서 빌드되었는지”를 확인하는 것이다. 이걸 확인해야 패치가 가능하다.</p>
<p>현재 로드된 모듈이 어느 패키지 소스에서 왔는지부터 확인했다.</p>
<pre><code class="language-bash">apt-cache show linux-modules-$(uname -r) | egrep -i &#39;^(Package|Version|Source):&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/38d158b8-0236-450a-9852-1715e76de78f/image.png" alt=""></p>
<p>이 드라이버는 <code>linux-hwe-6.17</code> 소스 트리에서 빌드된 결과물이다. 그리고 실제 소스 트리를 받기 위해 다음을 실행했다.</p>
<pre><code class="language-bash">apt-get source linux-hwe-6.17</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/248235b4-4eca-43bf-b0c3-d209429fad1d/image.png" alt=""></p>
<p>Ubuntu는 기본적으로 소스 패키지 저장소(<code>deb-src</code>)를 비활성화해 두기 때문에 <code>apt-get source</code>가 바로 동작하지 않는다. 따라서 <code>sources.list</code>에 <code>deb-src</code>를 활성화해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/f267d9f7-f8e2-4925-8b6a-6966c6735225/image.png" alt=""></p>
<p>Ubuntu 24.04부터는 예전 방식(<code>/etc/apt/sources.list</code>)이 아니라
새로운 <code>*.sources</code> 파일 형식을 사용한다. 그래서 <code>sources.list</code>를 수정하는 게 아니라 <code>ubuntu.sources</code> 파일을 수정해야 한다.</p>
<pre><code class="language-bash">sudo nano /etc/apt/sources.list.d/ubuntu.sources</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/68960e39-73f0-46ab-844f-2cbfb3ee6314/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/e4dbb8d3-b663-4991-9346-9e0f6ae3617c/image.png" alt=""></p>
<p>Types 줄에 <code>deb-src</code>를 추가하고 저장한다.</p>
<pre><code class="language-bash">sudo apt update</code></pre>
<p>이제 source repository가 활성화된다.</p>
<pre><code class="language-bash">apt-get source linux-hwe-6.17</code></pre>
<p>현재 디렉터리에 <code>linux-hwe-6.17-6.17.x</code> 폴더가 생성되었다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c69e99c9-0684-47da-ad79-66f3892b4b20/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/297bcb57-f938-4388-9b7e-ff44071643e1/image.png" alt=""></p>
<p>폴더 내부에서 <code>drivers/media/i2c/ov02c10.c</code> 경로에서 우리가 수정해야할 드라이브를 찾을 수 있다.</p>
<pre><code class="language-c">// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2022 Intel Corporation.

#include &lt;linux/acpi.h&gt;
#include &lt;linux/clk.h&gt;
#include &lt;linux/delay.h&gt;
#include &lt;linux/gpio/consumer.h&gt;
#include &lt;linux/i2c.h&gt;
#include &lt;linux/module.h&gt;
#include &lt;linux/pm_runtime.h&gt;
#include &lt;linux/regmap.h&gt;
#include &lt;linux/version.h&gt;
#include &lt;media/v4l2-cci.h&gt;
#include &lt;media/v4l2-ctrls.h&gt;
#include &lt;media/v4l2-device.h&gt;
#include &lt;media/v4l2-fwnode.h&gt;

#define OV02C10_LINK_FREQ_400MHZ    400000000ULL
#define OV02C10_MCLK            19200000

/* 중략 */

        /* Try to force expected MCLK if a controllable clock is provided */
    if (ov02c10-&gt;img_clk &amp;&amp; mclk != OV02C10_MCLK) {
        ret = clk_set_rate(ov02c10-&gt;img_clk, OV02C10_MCLK);
        if (!ret)
            mclk = clk_get_rate(ov02c10-&gt;img_clk);
    }

    /*
     * Upstream driver expects 19.2MHz, but some platforms wire 26MHz.
     * Allow 26MHz as a pragmatic quirk so probe can continue.
     */
    if (mclk != OV02C10_MCLK &amp;&amp; mclk != 26000000) {
        fwnode_handle_put(ep);
        return dev_err_probe(dev, -EINVAL,
                     &quot;external clock %u is not supported\n&quot;,
                     mclk);
    }

    if (mclk == 26000000)
        dev_warn(dev, &quot;using non-standard external clock %uHz (expected %uHz)\n&quot;,
             mclk, OV02C10_MCLK);

    ret = v4l2_fwnode_endpoint_alloc_parse(ep, &amp;bus_cfg);
    fwnode_handle_put(ep);
    if (ret)
        return dev_err_probe(dev, ret, &quot;parsing endpoint failed\n&quot;);

/* 중략 */

module_i2c_driver(ov02c10_i2c_driver);

MODULE_AUTHOR(&quot;Hao Yao &lt;hao.yao@intel.com&gt;&quot;);
MODULE_AUTHOR(&quot;Heimir Thor Sverrisson &lt;heimir.sverrisson@gmail.com&gt;&quot;);
MODULE_AUTHOR(&quot;Hans de Goede &lt;hansg@kernel.org&gt;&quot;);
MODULE_DESCRIPTION(&quot;OmniVision OV02C10 sensor driver&quot;);
MODULE_LICENSE(&quot;GPL&quot;);</code></pre>
<p><code>ov02c10_check_hwcfg()</code> 로직을 수정하였다. 해당 함수는 드라이버가 센서를 실제로 등록하기 전에, 플랫폼이 제공하는 하드웨어 구성(클럭 주파수, CSI2 lane 구성, link frequency 등)이 드라이버가 가정하는 조건과 일치하는지를 점검하는 “게이트” 역할을 한다. 여기서 -EINVAL로 리턴되면 probe는 즉시 중단되고, 이후 계층(CamHAL, icamerasrc, libcamera)은 정상적인 센서 파이프라인을 열 기회를 얻지 못한다.</p>
<p>클럭을 강제로 맞출 수 없거나(하드웨어 고정), ACPI/펌웨어가 이미 26MHz로 설정해 둔 플랫폼을 고려해, 기존에는 즉시 실패하던 조건을 완화하여 26MHz를 예외적으로 허용하도록 변경했다. 즉, 19.2MHz만 허용하던 “하드 실패(hard fail)” 정책을 “경고 후 진행(warn-and-continue)” 정책으로 바꾸었다.</p>
<h3 id="3-2-수정된-소스-코드를-시스템에-적용하기-dkms-활용">3-2. 수정된 소스 코드를 시스템에 적용하기 (DKMS 활용)</h3>
<p><code>ov02c10.c</code> 소스를 수정했다고 해서 바로 시스템에 반영되는 것은 아니다. 리눅스 커널 드라이버는 커널 모듈 형태로 빌드되어 <code>/lib/modules/&lt;kernel-version&gt;/</code> 아래에 설치되기 때문에, 수정한 코드를 실제로 사용하려면 해당 모듈을 다시 빌드하여 커널이 로드하도록 만들어야 한다.</p>
<p>이때 선택지는 크게 두 가지가 있다.</p>
<ol>
<li>커널 전체를 다시 빌드하는 방법</li>
<li>해당 드라이버 모듈만 별도로 빌드하여 덮어쓰는 방법</li>
</ol>
<p>첫 번째 방법은 안정적이지만 시간과 작업량이 크다. 이번 경우처럼 특정 센서 드라이버 하나만 수정한 상황에서는 모듈만 재빌드하는 방식이 훨씬 현실적이다.</p>
<p>또한 단순히 한 번 빌드해 <code>/lib/modules</code>에 복사하는 방식은 커널 업데이트 시 다시 원복될 가능성이 있다. 이 문제를 해결하기 위해 사용하는 것이 DKMS(Dynamic Kernel Module Support)이다.</p>
<p>DKMS는 수정한 모듈 소스를 시스템에 등록해 두고,</p>
<ul>
<li>새로운 커널이 설치되면</li>
<li>자동으로 해당 커널 헤더에 맞게 모듈을 다시 빌드하고</li>
<li><code>/lib/modules/&lt;kernel&gt;/updates/dkms/</code> 아래에 설치한다.</li>
</ul>
<p>즉, 커널 업데이트 이후에도 패치가 유지되는 구조를 만들 수 있다.</p>
<h4 id="1-dkms-및-빌드-환경-준비">(1) DKMS 및 빌드 환경 준비</h4>
<p>먼저 모듈을 빌드할 수 있도록 필요한 패키지를 설치한다.</p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y dkms build-essential linux-headers-$(uname -r)</code></pre>
<ul>
<li><code>dkms</code> : 모듈 자동 재빌드 관리</li>
<li><code>build-essential</code> : gcc, make 등 기본 빌드 도구</li>
<li><code>linux-headers</code> : 현재 커널에 맞는 커널 헤더</li>
</ul>
<h4 id="2-dkms-소스-디렉터리-생성">(2) DKMS 소스 디렉터리 생성</h4>
<p>DKMS는 <code>/usr/src/&lt;module-name&gt;-&lt;version&gt;/</code> 경로에 소스를 저장한다</p>
<pre><code class="language-bash">sudo mkdir -p /usr/src/ov02c10-1.0</code></pre>
<p>여기서 <code>1.0</code>은 임의의 버전이다. 추후 패치를 수정하면 <code>1.1</code>, <code>1.2</code> 등으로 올릴 수 있다.</p>
<h4 id="3-수정된-드라이버-소스-복사">(3) 수정된 드라이버 소스 복사</h4>
<p>앞서 받아 둔 커널 소스 트리에서 수정한 <code>ov02c10.c</code>를 복사한다.</p>
<pre><code class="language-bash">sudo cp ~/linux-hwe-6.17-6.17.x/drivers/media/i2c/ov02c10.c \
        /usr/src/ov02c10-1.0/</code></pre>
<h4 id="4-dkms용-makefile-작성">(4) DKMS용 Makefile 작성</h4>
<p>DKMS가 모듈을 
빌드할 때 사용할 Makefile을 작성한다.</p>
<pre><code class="language-bash">sudo nano /usr/src/ov02c10-1.0/Makefile</code></pre>
<p>내용은 다음과 같다.</p>
<pre><code class="language-makefile">obj-m += ov02c10.o

KDIR := /lib/modules/$(KERNEL_VERSION)/build
PWD  := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean</code></pre>
<p>여기서 <code>$(KERNEL_VERSION)</code>은 DKMS가 빌드 시 자동으로 전달한다. </p>
<h4 id="5-dkmsconf-작성">(5) dkms.conf 작성</h4>
<p>DKMS에게 이 모듈을 어떻게 빌드하고 어디에 설치할지 알려주는 설정 파일을 만든다.</p>
<pre><code class="language-bash">sudo nano /usr/src/ov02c10-1.0/dkms.conf</code></pre>
<p>내용은 다음과 같다</p>
<pre><code class="language-bash">PACKAGE_NAME=&quot;ov02c10&quot;
PACKAGE_VERSION=&quot;1.0&quot;

BUILT_MODULE_NAME[0]=&quot;ov02c10&quot;
DEST_MODULE_LOCATION[0]=&quot;/kernel/drivers/media/i2c&quot;

AUTOINSTALL=&quot;yes&quot;</code></pre>
<p><code>AUTOINSTALL=&quot;yes&quot;</code>는 매우 중요한 옵션이다. 커널이 업데이트될 때마다 자동으로 모듈을 다시 빌드하도록 지시한다.</p>
<h4 id="6-dkms-등록-및-빌드">(6) DKMS 등록 및 빌드</h4>
<p>이제 DKMS에 모듈을 등록하고 빌드를 수행한다.</p>
<pre><code class="language-bash">sudo dkms add -m ov02c10 -v 1.0
sudo dkms build -m ov02c10 -v 1.0
sudo dkms install -m ov02c10 -v 1.0</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/4b1ec653-c3f8-4586-a37a-7d370cc12336/image.png" alt=""></p>
<p>정상적으로 완료되면 DKMS는 수정된 <code>ov02c10</code> 모듈을 다음 위치에 설치한다</p>
<pre><code>/lib/modules/&lt;kernel-version&gt;/updates/dkms/</code></pre><p>이 경로에 설치된 모듈은 기본 커널 모듈보다 우선적으로 로드된다.</p>
<h4 id="7-시스템-재부팅-및-적용-확인">(7) 시스템 재부팅 및 적용 확인</h4>
<p>모듈이 실제로 적용되었는지 확인하기 위해 시스템을 재부팅한다.</p>
<pre><code class="language-bash">sudo reboot</code></pre>
<p>부팅 후 다시 <code>dmesg</code>를 확인한다.</p>
<pre><code class="language-bash">dmesg | grep -i ov02c10</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/aea2f1b1-b25b-492e-89ae-2d3130747240/image.png" alt=""></p>
<p>현재 dmesg를 보면 가장 중요한 변화는 이미 확인되었다. 기존의 핵심 실패 원인이었던</p>
<pre><code>external clock 26000000 is not supported</code></pre><p>로그가 더 이상 나타나지 않고, 대신 다음과 같이 출력된다.</p>
<pre><code>using non-standard external clock 26000000Hz (expected 19200000Hz)</code></pre><p>부팅 초기에 ov02c10 probe가 실행될 때, 우리가 DKMS로 설치한 수정 모듈이 실제로 사용되었음을 의미한다. 즉, 26MHz 클럭 검증 게이트는 더 이상 probe를 중단시키지 않는다.</p>
<p>이어지는 로그:</p>
<pre><code>supply dovdd not found, using dummy regulator
supply dvdd not found, using dummy regulator</code></pre><p>이 단계는 probe가 “전원/레귤레이터 확인 단계”로 진입했음을 보여준다. ACPI나 DT에 해당 regulator가 명시되지 않았거나, 플랫폼이 상시전원 구조이기 때문에 더미 레귤레이터로 진행하는 상태다. 여기까지는 실패가 아니라 진행 중 로그다.</p>
<h4 id="8-dkms-상태-확인">(8) DKMS 상태 확인</h4>
<p>설치된 DKMS 모듈은 다음 명령으로 확인할 수 있다.</p>
<pre><code class="language-bash">dkms status</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/8e014852-8c74-42a0-91bc-ab8f41a2cd38/image.png" alt=""></p>
<p>이 상태라면 이후 커널 업데이트가 발생하더라도 DKMS가 자동으로 해당 모듈을 재빌드하여 유지한다.</p>
<p>다음 단계에서는 실제로 센서가 media graph에 등록되었는지와 libcamera 경로가 정상 동작하는지를 확인한다.</p>
<h3 id="3-3-media-graph-및-libcamera-경로-확인">3-3. media graph 및 libcamera 경로 확인</h3>
<p>DKMS를 통해 수정된 <code>ov02c10</code> 드라이버 모듈을 시스템에 적용했다면, 다음 단계는 실제로 센서가 커널의 media graph에 등록되었는지와 userspace 카메라 스택(libcamera)이 정상적으로 동작하는지를 확인하는 것이다.</p>
<p>센서 probe가 더 이상 초기 단계에서 실패하지 않는다고 해서 곧바로 카메라가 정상 동작하는 것은 아니다. 커널 레벨에서 센서가 등록된 이후에도 다음과 같은 여러 계층을 거쳐야 실제 영상이 사용자 프로그램까지 전달된다.</p>
<ul>
<li>센서 드라이버 (<code>ov02c10</code>)</li>
<li>IPU6 capture 파이프라인 (<code>ipu6</code>, <code>isys</code>, <code>psys</code>)</li>
<li>V4L2 / media controller</li>
<li>libcamera</li>
<li>PipeWire / GStreamer</li>
<li>사용자 애플리케이션 (Discord, OBS 등)</li>
</ul>
<p>따라서 이 단계에서는 먼저 커널 media graph에 센서가 실제로 등록되었는지부터 확인한다.</p>
<h4 id="1-media-graph에서-센서-확인">(1) media graph에서 센서 확인</h4>
<p>다음 명령으로 현재 카메라 파이프라인을 출력할 수 있다.</p>
<pre><code class="language-bash">media-ctl -p -d /dev/media0</code></pre>
<p>출력 결과에는 IPU6 capture 노드와 함께 여러 sub-device들이 나타난다. 여기서 중요한 것은 <code>ov02c10</code> 또는 <code>OVTI02C1</code>과 같은 센서 엔티티가 실제로 등록되어 있는지 여부이다.</p>
<p>필터링해서 보면 다음과 같이 확인할 수 있다.</p>
<pre><code class="language-bash">media-ctl -p -d /dev/media0 | grep -i ov02</code></pre>
<p>또는</p>
<pre><code class="language-bash">media-ctl -p -d /dev/media0 | grep -i sensor</code></pre>
<p>센서가 정상적으로 등록되었다면 media graph 어딘가에 <code>ov02c10</code> 관련 엔티티가 나타난다.
만약 아무 결과도 나오지 않는다면 probe는 여전히 중간 단계에서 실패했을 가능성이 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/36f85aa9-f1a1-4ade-ade8-cb9bdd91e435/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/6b168c3e-f13c-461e-a34b-03b7933ef03d/image.png" alt=""></p>
<p><code>media-ctl</code> 출력에서 <code>ov02c10</code> 센서 엔티티가 확인되고, 해당 센서가 <code>Intel IPU6 CSI2</code> 노드와 <code>[ENABLED]</code> 상태로 연결되어 있는 것을 확인했다. 이는 커널 레벨에서 센서 드라이버 probe와 media graph 등록이 정상적으로 완료되었음을 의미한다.</p>
<h4 id="2-libcamera에서-카메라-장치-확인">(2) libcamera에서 카메라 장치 확인</h4>
<p>센서가 media graph에 등록되어 있다면, 다음 단계는 libcamera가 해당 장치를 인식하는지 확인하는 것이다.</p>
<pre><code class="language-bash">cam -l</code></pre>
<p>정상적인 경우 다음과 같이 내부 카메라가 하나 이상 표시된다.</p>
<pre><code>Available cameras:
1: Internal front camera</code></pre><p>이 단계에서 카메라가 나타난다면 최소한 다음 경로는 정상적으로 연결된 것이다</p>
<pre><code>sensor → IPU6 → V4L2 → libcamera</code></pre><p>만약 카메라 목록이 비어 있다면 다음 두 가지 가능성을 의심해야 한다.</p>
<ul>
<li>센서 probe가 실제로는 실패했거나</li>
<li>libcamera userspace 스택이 깨져 있는 경우</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/6a36e292-7b4e-488e-9698-29798d33fe8a/image.png" alt=""></p>
<p>여기서 두 가지 중요한 사실을 확인할 수 있다.</p>
<p>첫째, 현재 실행 중인 libcamera 버전은 Ubuntu 기본 패키지에 포함된 libcamera 0.2.0이다.</p>
<p>둘째, libcamera가 IPA(Image Processing Algorithm) 모듈을 찾지 못하고 있다.</p>
<p>IPA는 센서에서 출력된 RAW 이미지를 처리하기 위한 ISP 알고리즘 모듈로, 노출 제어(AE), 화이트 밸런스(AWB), 색 보정(CCM) 등의 기능을 담당한다. 이 모듈이 로드되지 않으면 libcamera는 카메라 파이프라인을 초기화할 수 없다.</p>
<p>실제로 <code>Available cameras:</code> 아래에 아무 장치도 표시되지 않는 것을 확인할 수 있다.</p>
<p>즉 현재 상태는 다음과 같이 정리할 수 있다.</p>
<pre><code>ov02c10 sensor driver        ✓ 정상
IPU6 media graph             ✓ 정상
libcamera pipeline           ✗ 실패</code></pre><p>따라서 문제의 원인은 더 이상 커널 드라이버가 아니라 libcamera userspace 스택에 있는 것으로 판단할 수 있다.</p>
<p>다음 단계에서는 Ubuntu 기본 libcamera 패키지와 수동 빌드한 libcamera 사이에서 발생할 수 있는 버전 충돌 문제를 정리하고, IPU6 카메라가 정상 동작하도록 libcamera 환경을 재구성한다.</p>
<h2 id="4-libcamera-userspace-스택-재구성">4. libcamera userspace 스택 재구성</h2>
<p>앞 단계에서 <code>cam -l</code> 결과를 통해 현재 userspace 카메라 스택이 Ubuntu 기본 libcamera 0.2 계열이며, 동시에 IPA 모듈이 로드되지 않는 상태임을 확인했다. 따라서 이 장에서는 먼저 현재 0.2 스택을 보완할 수 있는지 확인하고, 필요할 경우 libcamera를 직접 빌드하는 방향으로 userspace 환경을 재구성한다.</p>
<h3 id="4-1-ubuntu-기본-libcamera-패키지-상태-확인">4-1. Ubuntu 기본 libcamera 패키지 상태 확인</h3>
<p>먼저 시스템에 설치된 libcamera 관련 패키지를 확인한다</p>
<pre><code class="language-bash">dpkg -l | grep libcamera</code></pre>
<p>출력 결과는 아래와 같다. 
<img src="https://velog.velcdn.com/images/dev_hammy/post/ca9e9ef2-c089-4ae6-b907-42a4b8d750b9/image.png" alt=""></p>
<p>즉 현재 시스템에는 Ubuntu 기본 패키지인 libcamera 0.2 계열만 설치되어 있다.</p>
<p>이 버전은 Ubuntu에서 기본 제공하는 userspace 카메라 스택으로, 일반적인 USB UVC 카메라 환경에서는 문제없이 동작한다. 그러나 IPU6 기반 노트북 카메라의 경우에는 다음과 같은 이유로 정상 동작하지 않는 경우가 많다.</p>
<ul>
<li>필요한 IPA(Image Processing Algorithm) 모듈이 포함되지 않은 경우</li>
<li>IPU6 파이프라인 핸들러가 제대로 활성화되지 않은 경우</li>
<li>libcamera와 커널 드라이버 사이의 버전 차이</li>
</ul>
<h3 id="4-2-libcamera-02용-ipa-패키지-설치-가능-여부-확인">4-2. libcamera 0.2용 IPA 패키지 설치 가능 여부 확인</h3>
<p>현재 설치된 패키지 목록에는 <code>libcamera-tools</code>, <code>libcamera0.2</code>만 보이고, <code>libcamera-ipa</code>는 보이지 않는다. 따라서 우선 배포판 저장소에서 IPA 패키지가 제공되는지 확인한다.</p>
<pre><code class="language-bash">apt-cache search libcamera | grep ipa</code></pre>
<p>또는</p>
<pre><code class="language-bash">apt-cache policy libcamera-ipa</code></pre>
<p>패키지가 존재한다면 다음과 같이 설치한다.</p>
<pre><code class="language-bash">sudo apt install libcamera-ipa</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/ce344e5b-a762-4ad0-8504-ce987b05fd97/image.png" alt=""></p>
<p>설치 후 다시 다음 명령으로 카메라 인식 여부를 확인한다.</p>
<pre><code class="language-bash">cam -l</code></pre>
<p>이 단계에서 카메라가 정상적으로 나타난다면, 별도의 직접 빌드 없이 Ubuntu 기본 0.2 스택만으로도 userspace 문제가 해결된 것이다. 반대로 여전히 <code>No IPA found</code> 또는 빈 카메라 목록이 유지된다면, 그때는 기본 패키지 조합만으로는 IPU6 파이프라인을 제대로 지원하지 못하는 것으로 보고 직접 빌드 단계로 넘어간다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/da9d1eec-80ab-4f3f-a5c3-272afdc14a89/image.png" alt=""></p>
<p>여기서 두 가지 사실을 확인할 수 있다.</p>
<p>첫째, 현재 시스템에서 사용 중인 libcamera는 Ubuntu 기본 패키지에 포함된 libcamera 0.2.0이다.</p>
<p>둘째, <code>Available cameras:</code> 아래에 어떤 장치도 표시되지 않는다.</p>
<p>이 결과는 단순히 IPA 모듈이 없는 문제가 아니라, libcamera가 카메라 파이프라인 자체를 생성하지 못하고 있다는 뜻이다.</p>
<p>앞 단계에서 확인했듯이 커널 레벨에서는 이미 다음 경로가 정상적으로 구성되어 있다.</p>
<pre><code>ov02c10 sensor driver        ✓ 정상
IPU6 media graph             ✓ 정상
/dev/video0                  ✓ 생성됨</code></pre><p>즉 센서와 IPU6 드라이버는 정상적으로 초기화되었으며, media graph에서도 센서 노드가 등록된 상태다.</p>
<p>따라서 현재 실패 지점은 커널이 아니라 libcamera userspace 파이프라인 초기화 단계라고 볼 수 있다.</p>
<p>Ubuntu 기본 libcamera 0.2는 일부 플랫폼에서는 문제없이 동작하지만, IPU6 기반 노트북 카메라 환경에서는 pipeline handler 지원이 충분하지 않은 경우가 있다. 이 경우 media graph에 장치가 존재하더라도 libcamera가 이를 실제 카메라로 등록하지 못한다.</p>
<p>따라서 다음 단계에서는 Ubuntu 기본 패키지 대신 libcamera를 직접 빌드하여 최신 userspace 스택으로 교체한다.</p>
<h3 id="4-3-libcamera-최신-버전으로-재구성">4-3. libcamera 최신 버전으로 재구성</h3>
<p>이 단계에서는 Ubuntu 기본 패키지를 그대로 유지하는 대신, libcamera를 직접 빌드하여 userspace 스택을 최신 버전으로 재구성한다.</p>
<h4 id="1-ubuntu-기본-libcamera-패키지-제거">(1) Ubuntu 기본 libcamera 패키지 제거</h4>
<p>먼저 기존 Ubuntu 패키지로 설치된 libcamera를 제거한다.</p>
<pre><code class="language-bash">sudo apt remove --purge libcamera0.2 libcamera-tools libcamera-ipa</code></pre>
<p>패키지를 제거한 뒤에는 불필요한 의존성을 정리한다.</p>
<pre><code class="language-bash">sudo apt autoremove</code></pre>
<p>이 과정을 통해 시스템에는 더 이상 Ubuntu 패키지로 설치된 libcamera 라이브러리가 남지 않게 된다. </p>
<p>이후에는 <code>/usr/local</code> 경로에 직접 빌드한 libcamera를 설치하여 userspace 카메라 스택을 구성하게 된다.</p>
<p>다음 단계에서는 libcamera 소스 코드를 다운로드하고, 최신 버전을 직접 빌드하여 시스템에 설치한다.</p>
<h4 id="2-libcamera-소스-코드-다운로드">(2) libcamera 소스 코드 다운로드</h4>
<p>Ubuntu 기본 패키지를 제거했다면, 이제 libcamera userspace 스택을 직접 빌드하여 설치할 수 있다. libcamera는 공식 Git 저장소에서 소스를 내려받아 빌드하는 방식으로 설치할 수 있다.</p>
<p>먼저 빌드에 필요한 기본 도구와 의존 패키지를 설치한다.</p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y \
    git meson ninja-build pkg-config \
    libdrm-dev libexif-dev \
    libjpeg-dev libtiff-dev \
    libpng-dev \
    libgnutls28-dev \
    libevent-dev \
    python3-pip python3-yaml \
    python3-jinja2</code></pre>
<p>이 패키지들은 libcamera의 핵심 라이브러리, GStreamer 연동, 이미지 포맷 처리, 그리고 빌드 시스템(meson)에서 사용하는 의존성들이다.</p>
<p>이제 libcamera 소스를 다운로드한다.</p>
<pre><code class="language-bash">cd /tmp
git clone https://git.libcamera.org/libcamera/libcamera.git
cd libcamera</code></pre>
<p>libcamera는 활발하게 개발되는 프로젝트이기 때문에, 특정 안정 버전을 체크아웃하여 빌드하는 것이 좋다. 여기서는 비교적 안정적인 v0.4.0 태그를 사용한다.</p>
<pre><code class="language-bash">git checkout v0.4.0</code></pre>
<p>이렇게 하면 <code>/tmp/libcamera</code> 디렉터리에 libcamera 0.4.0 소스 트리가 준비된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/d6713f7b-1313-4131-9d4e-77a9c8667d43/image.png" alt=""></p>
<h4 id="3-libcamera-빌드">(3) libcamera 빌드</h4>
<p>소스를 다운로드했다면 meson과 ninja를 이용해 빌드를 진행한다. 먼저 빌드 디렉터리를 생성하고 설정을 수행한다.</p>
<pre><code class="language-bash">meson setup build \
    -Dpipelines=auto \
    -Dipas=auto \
    -Dgstreamer=enabled \
    -Dv4l2=true \
    --prefix=/usr/local</code></pre>
<p>여기서 주요 옵션의 의미는 다음과 같다.</p>
<ul>
<li><p><code>Dpipelines=auto</code> : 시스템에서 사용할 수 있는 pipeline handler를 자동으로 활성화한다.</p>
</li>
<li><p><code>Dipas=auto</code> : 사용 가능한 IPA(Image Processing Algorithm) 모듈을 자동으로 빌드한다.</p>
</li>
<li><p><code>Dgstreamer=enabled</code> : GStreamer 플러그인을 함께 빌드하여 멀티미디어 파이프라인에서 libcamera를 사용할 수 있도록 한다.</p>
</li>
<li><p><code>-prefix=/usr/local</code> : Ubuntu 기본 패키지와 충돌하지 않도록 <code>/usr/local</code> 경로에 설치한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/0094c244-442c-4bd9-a33c-e7a970cfcfbe/image.png" alt=""></p>
<p>빌드 과정에서 발생한 에러메시지는 다음과 같다.</p>
<pre><code>ERROR: Options &quot;auto&quot; are not in allowed choices: &quot;ipu3, mali-c55, rkisp1, rpi/vc4, simple, vimc&quot;</code></pre><p>이 메시지는 현재 체크아웃한 libcamera v0.4.0 소스 트리에서 <code>auto</code>가 허용되는 옵션이 아니며, 동시에 이 버전의 공식 소스 트리에는 <code>ipu6</code> pipeline handler가 직접 포함되어 있지 않다는 뜻이다.</p>
<p>이 경우 libcamera는 특정 ISP에 특화된 pipeline 대신, generic media graph를 해석할 수 있는 <code>simple</code> pipeline handler를 사용해 카메라를 구성할 수 있다.</p>
<p><code>simple</code> pipeline은 다음과 같은 특징을 가진다.</p>
<ul>
<li>특정 ISP 구조에 의존하지 않는다</li>
<li>V4L2 media controller graph를 직접 해석한다</li>
<li>RAW 센서 + capture node 구조의 장치를 범용적으로 처리할 수 있다</li>
</ul>
<p>즉 IPU6가 전용 pipeline handler로 지원되지 않는 환경에서도, media graph가 정상적으로 구성되어 있다면 <code>simple</code> pipeline을 통해 카메라 파이프라인을 생성할 수 있다.</p>
<p>앞 단계에서 이미 다음 사실을 확인했다.</p>
<pre><code>ov02c10 sensor driver        ✓ 정상
IPU6 media graph             ✓ 정상
/dev/video0                  ✓ 생성됨</code></pre><p>즉 커널 레벨에서 media graph가 정상적으로 구성되어 있기 때문에, libcamera userspace에서는 generic media graph 기반 pipeline인 <code>simple</code> handler를 사용하여 카메라 파이프라인을 생성하는 접근이 가능하다.</p>
<p>따라서 libcamera 빌드 옵션을 다음과 같이 조정한다.</p>
<pre><code class="language-bash">rm -rf build

meson setup build \
    -Dpipelines=simple \
    -Dipas=simple \
    -Dgstreamer=enabled \
    -Dv4l2=true \
    --prefix=/usr/local</code></pre>
<p>이 설정은 libcamera가 <code>simple</code> pipeline과 simple IPA 모듈을 함께 빌드하도록 하여, IPU6 media graph를 기반으로 userspace 카메라 파이프라인을 구성할 수 있도록 한다.</p>
<p>참고로<code>cmake</code>가 없다면 빌드에 실패하므로 libcamera 빌드에서 자주 필요한 도구들과 의존성들을 <strong>먼저</strong> 함께 설치한다. </p>
<pre><code class="language-bash">sudo apt install cmake pkg-config python3-yaml python3-jinja2 python3-ply

sudo apt install \
libglib2.0-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libdrm-dev \
libexif-dev \
libjpeg-dev \
libtiff-dev \
libpng-dev \
libgnutls28-dev \
libevent-dev \
libyaml-dev</code></pre>
<p>이미 실패한 빌드 디렉터리가 있다면 먼저 삭제하고 다시 빌드를 진행한다.</p>
<pre><code class="language-bash">cd /tmp/libcamera
rm -rf build

meson setup build \
    -Dpipelines=simple \
    -Dipas=simple \
    -Dgstreamer=enabled \
    -Dv4l2=true \
    --prefix=/usr/local</code></pre>
<p>libcamera는 다양한 기능(GStreamer 연동, 이미지 포맷 지원 등)을 선택적으로 빌드하기 때문에, 시스템에 필요한 개발 패키지가 없으면 Meson 단계에서 의존성 오류가 발생한다. 이런 경우에는 오류 메시지에 나타난 라이브러리의 <code>-dev</code> 패키지를 설치한 뒤 Meson 설정을 다시 실행하면 된다.</p>
<pre><code>madhamster@treadmill:/tmp/libcamera$ meson setup build     -Dpipelines=simple     -Dipas=simple     -Dgstreamer=enabled     -Dv4l2=true     --prefix=/usr/local
The Meson build system
Version: 1.3.2
Source dir: /tmp/libcamera
Build dir: /tmp/libcamera/build
Build type: native build
Project name: libcamera
Project version: 0.4.0
C compiler for the host machine: cc (gcc 13.3.0 &quot;cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0&quot;)
C linker for the host machine: cc ld.bfd 2.42
C++ compiler for the host machine: c++ (gcc 13.3.0 &quot;c++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0&quot;)
C++ linker for the host machine: c++ ld.bfd 2.42
Host machine cpu family: x86_64
Host machine cpu: x86_64
Header &quot;fcntl.h&quot; has symbol &quot;F_ADD_SEALS&quot; : YES 
Header &quot;unistd.h&quot; has symbol &quot;issetugid&quot; : NO 
Header &quot;locale.h&quot; has symbol &quot;locale_t&quot; : YES 
Header &quot;sys/mman.h&quot; has symbol &quot;memfd_create&quot; : YES 
Header &quot;stdlib.h&quot; has symbol &quot;secure_getenv&quot; : YES 
Compiler for C supports arguments -Wno-c99-designator: NO 
Found pkg-config: YES (/usr/bin/pkg-config) 1.8.1
Found CMake: /usr/bin/cmake (3.28.3)
Run-time dependency lttng-ust found: NO (tried pkgconfig and cmake)
Program ./parser.py found: YES (/tmp/libcamera/utils/codegen/ipc/./parser.py)
Program ./generate.py found: YES (/tmp/libcamera/utils/codegen/ipc/./generate.py)
Program ./extract-docs.py found: YES (/tmp/libcamera/utils/codegen/ipc/./extract-docs.py)
Configuring version.h using configuration
Program openssl found: YES (/usr/bin/openssl)
Run-time dependency libyuv found: NO (tried pkgconfig and cmake)
Library atomic found: YES
Run-time dependency threads found: YES
Run-time dependency libdw found: YES 0.190
Run-time dependency libunwind found: YES 1.6.2
Header &quot;execinfo.h&quot; has symbol &quot;backtrace&quot; : YES 
Checking for function &quot;dlopen&quot; : YES 
Run-time dependency libudev found: YES 255
Run-time dependency yaml-0.1 found: YES 0.2.5
Run-time dependency gnutls found: YES 3.8.3
Dependency libexif skipped: feature android disabled
Dependency libjpeg skipped: feature android disabled
Run-time dependency libevent_pthreads found: YES 2.1.12-stable
Run-time dependency libtiff-4 found: YES 4.5.1
Run-time dependency GTest found: NO (tried pkgconfig and system)
Looking for a fallback subproject for the dependency gtest

Executing subproject gtest 

gtest| Project name: gtest
gtest| Project version: 1.11.0
gtest| C++ compiler for the host machine: c++ (gcc 13.3.0 &quot;c++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0&quot;)
gtest| C++ linker for the host machine: c++ ld.bfd 2.42
gtest| Dependency threads found: YES unknown (cached)
gtest| Dependency threads found: YES unknown (cached)
gtest| Dependency threads found: YES unknown (cached)
gtest| Dependency threads found: YES unknown (cached)
gtest| Build targets in project: 36
gtest| Subproject gtest finished.

Dependency gtest from subproject subprojects/googletest-release-1.11.0 found: YES 1.11.0
Run-time dependency libdrm found: YES 2.4.125
Run-time dependency libjpeg found: YES 2.1.5
sdl2-config found: NO
Run-time dependency sdl2 found: NO (tried pkgconfig, config-tool and cmake)
Run-time dependency qt6 (modules: Core, Gui, OpenGL, OpenGLWidgets, Widgets) found: NO (tried pkgconfig)
Run-time dependency glib-2.0 found: YES 2.80.0
Run-time dependency gstreamer-video-1.0 found: YES 1.24.2
Run-time dependency gstreamer-allocators-1.0 found: YES 1.24.2
Run-time dependency python3 found: YES 3.12
pybind11-config found: NO
Run-time dependency pybind11 found: NO (tried pkgconfig, config-tool and cmake)
Configuring libcamerify using configuration
Program doxygen found: NO
Program dot found: NO
Program sphinx-build-3 found: NO
Program sphinx-build found: NO
Configuring config.h using configuration
Program python3 (jinja2, yaml, jinja2, ply) found: YES (/usr/bin/python3) modules: jinja2, yaml, jinja2, ply
Build targets in project: 42

libcamera 0.4.0

  Versions
    Sources                  : 0.4.0

  Paths
    LIBCAMERA_DATA_DIR       : &quot;/usr/local/share/libcamera&quot;
    LIBCAMERA_SYSCONF_DIR    : &quot;/usr/local/etc/libcamera&quot;
    IPA_PROXY_DIR            : &quot;/usr/local/libexec/libcamera&quot;
    IPA_CONFIG_DIR           : &quot;/usr/local/etc/libcamera/ipa:/usr/local/share/libcamera/ipa&quot;
    IPA_MODULE_DIR           : &quot;/usr/local/lib/x86_64-linux-gnu/libcamera&quot;

  Configuration
    SoftISP support          : true
    IPA modules signed with  : gnutls
    Enabled pipelines        : simple
    Enabled IPA modules      : simple
    Controls files           : control_ids_core.yaml
                               control_ids_debug.yaml
                               control_ids_draft.yaml
    Properties files         : property_ids_draft.yaml
                               property_ids_core.yaml
    Hotplug support          : YES
    Tracing support          : NO
    Android support          : NO
    GStreamer support        : YES
    Python bindings          : NO
    V4L2 emulation support   : YES
    cam application          : YES
    qcam application         : NO
    lc-compliance application: YES
    Unit tests               : NO

  Subprojects
    gtest                    : YES

  User defined options
    prefix                   : /usr/local
    gstreamer                : enabled
    ipas                     : simple
    pipelines                : simple
    v4l2                     : true

Found ninja-1.11.1 at /usr/bin/ninja</code></pre><p>이번 설정 로그에서 가장 중요한 부분은 아래 항목들이다.</p>
<pre><code>Enabled pipelines        : simple
Enabled IPA modules      : simple
GStreamer support        : YES
V4L2 emulation support   : YES
cam application          : YES</code></pre><p>이 설정은 현재 libcamera가 다음 구성을 기준으로 빌드될 것임을 의미한다.</p>
<ul>
<li><code>simple</code> pipeline handler 사용</li>
<li><code>simple</code> IPA 모듈 사용</li>
<li>GStreamer 연동 활성화</li>
<li>V4L2 emulation 지원 활성화</li>
<li><code>cam</code> 테스트 도구 포함</li>
</ul>
<p>앞서 확인했듯이 사용 중인 libcamera 0.4.0 공식 소스 트리에는 IPU6 전용 pipeline handler가 직접 포함되어 있지 않다. 따라서 이 단계에서는 IPU6 전용 handler를 활성화하는 대신, 이미 정상적으로 구성된 media graph를 기반으로 동작할 수 있는 generic pipeline handler인 <code>simple</code>을 사용하도록 설정한 것이다.</p>
<p>로그의 경로 정보도 중요하다.</p>
<pre><code>LIBCAMERA_DATA_DIR       : &quot;/usr/local/share/libcamera&quot;
LIBCAMERA_SYSCONF_DIR    : &quot;/usr/local/etc/libcamera&quot;
IPA_PROXY_DIR            : &quot;/usr/local/libexec/libcamera&quot;
IPA_CONFIG_DIR           : &quot;/usr/local/etc/libcamera/ipa:/usr/local/share/libcamera/ipa&quot;
IPA_MODULE_DIR           : &quot;/usr/local/lib/x86_64-linux-gnu/libcamera&quot;</code></pre><p>즉 설치가 완료되면 libcamera 관련 파일은 Ubuntu 기본 패키지 경로(<code>/usr/lib</code>, <code>/usr/bin</code>)가 아니라 <code>/usr/local</code> 아래에 배치된다. 이는 배포판 기본 libcamera와 직접 빌드한 libcamera를 논리적으로 분리하기 위한 것이다.</p>
<p>이제 설정 단계는 끝났으므로, 다음 단계에서는 실제 컴파일과 설치를 수행한다.</p>
<pre><code class="language-bash">ninja -C build</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/cd8d8a1d-5fad-4226-9bcb-89d9fbefd0c7/image.png" alt=""></p>
<p>이 명령은 <code>/tmp/libcamera/build</code> 디렉터리의 설정을 바탕으로 libcamera 라이브러리, <code>cam</code> 도구, GStreamer 플러그인, 그리고 simple IPA 모듈을 컴파일한다.</p>
<p>컴파일이 끝나면 다음 명령으로 시스템에 설치한다.</p>
<pre><code class="language-bash">sudo ninja -C build install
sudo ldconfig</code></pre>
<p>설치가 완료되면 주요 파일들은 다음과 같은 경로에 배치된다.</p>
<ul>
<li><p>libcamera 라이브러리 : <code>/usr/local/lib/x86_64-linux-gnu/</code></p>
</li>
<li><p>IPA 모듈 : <code>/usr/local/lib/x86_64-linux-gnu/libcamera/</code></p>
</li>
<li><p>IPA proxy : <code>/usr/local/libexec/libcamera/</code></p>
</li>
<li><p>설정 및 데이터 파일 : 
<code>/usr/local/share/libcamera/</code></p>
</li>
<li><p>실행 파일 (<code>cam</code>, <code>libcamerify</code> 등) : <code>/usr/local/bin/</code></p>
</li>
</ul>
<p>이 단계까지 완료되면 시스템은 더 이상 Ubuntu 기본 패키지의 libcamera 0.2만을 사용하는 것이 아니라, <code>/usr/local</code>에 직접 설치한 libcamera 0.4.0 기반 userspace 스택을 사용할 준비가 된 상태가 된다.</p>
<h4 id="4-설치-결과-확인">(4) 설치 결과 확인</h4>
<p>설치가 끝난 뒤에는 먼저 libcamera 관련 파일이 실제로 생성되었는지 확인한다. <code>ldconfig</code> 명령은 정상적으로 실행되더라도 별도의 성공 메시지를 출력하지 않는 경우가 많으므로, 실제 파일과 라이브러리 경로를 직접 확인하는 것이 더 확실하다.</p>
<p>먼저 IPA 모듈이 설치되었는지 확인한다.</p>
<pre><code class="language-bash">ls -al /usr/local/lib/x86_64-linux-gnu/libcamera/</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c7adb5b7-6071-4693-b755-c02521f3a22f/image.png" alt=""></p>
<p>정상적으로 설치되었다면 최소한 다음 파일들이 보여야 한다.</p>
<ul>
<li><code>ipa_soft_simple.so</code></li>
<li><code>ipa_soft_simple.so.sign</code>
이 파일들은 simple pipeline에서 사용하는 IPA 모듈과 서명 파일이다.</li>
</ul>
<p>다음으로 라이브러리 로더에 새 libcamera가 등록되었는지 확인한다.</p>
<pre><code class="language-bash">ldconfig -p | grep libcamera</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/5e62a6f2-bbe5-463c-b8d0-e0e5f2d96784/image.png" alt=""></p>
<p>정상적인 경우 <code>/usr/local/lib/x86_64-linux-gnu</code> 아래의 <code>libcamera.so.0.4</code> 와 <code>libcamera-base.so.0.4</code> 가 표시된다.</p>
<p>또한 실제 <code>cam</code> 실행 파일이 어디를 가리키는지도 함께 확인한다.</p>
<pre><code class="language-bash">which cam
ldd $(which cam) | grep libcamera</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/55151ec7-cfc0-4698-96ba-b199dea190a9/image.png" alt=""></p>
<p>이때 <code>cam</code>이 <code>/usr/local/bin/cam</code> 을 가리키고, <code>ldd</code> 결과에서 <code>/usr/local/lib/x86_64-linux-gnu/libcamera.so.0.4</code> 가 나타나면 직접 빌드한 libcamera 0.4 userspace 스택이 실제로 사용되는 상태라고 볼 수 있다.</p>
<h4 id="5-libcamera에서-카메라-장치-인식-확인">(5) libcamera에서 카메라 장치 인식 확인</h4>
<p>libcamera 0.4 설치가 끝난 뒤 다시 <code>cam -l</code>을 실행하여 카메라 장치 인식 여부를 확인했다</p>
<pre><code class="language-bash">cam -l</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/88d483d6-2fd4-4769-805f-7de1ad8202fd/image.png" alt=""></p>
<p>출력 결과는 다음과 같다.</p>
<pre><code>Available cameras:
1: Internal front camera (\_SB_.PC00.LNK0)</code></pre><p>이 결과는 libcamera가 실제 카메라 장치를 정상적으로 등록했음을 의미한다. 즉 다음 경로가 모두 정상적으로 연결된 상태다.</p>
<pre><code>ov02c10 sensor
→ IPU6 media graph
→ libcamera pipeline
→ userspace camera device</code></pre><p>로그에는 여러 경고 메시지가 함께 출력된다. 예를 들어 다음과 같은 메시지가 나타난다.</p>
<pre><code>The sensor kernel driver needs to be fixed
No static properties available for &#39;ov02c10&#39;
Failed to retrieve the sensor crop rectangle</code></pre><p>이 메시지들은 센서 드라이버가 일부 메타데이터(예: crop 영역, sensor properties)를 제공하지 않기 때문에 발생하는 경고이다. 그러나 실제 카메라 장치 생성에는 영향을 주지 않는다. 또한 다음 메시지도 확인할 수 있다.</p>
<pre><code>Could not open any dma-buf provider
Failed to create DmaBufAllocator</code></pre><p>이는 하드웨어 ISP나 GPU 기반 DMA 버퍼를 사용할 수 없는 환경에서 나타나는 로그로, libcamera가 software pipeline으로 동작하도록 fallback 했음을 의미한다.</p>
<p>중요한 점은 카메라 장치 자체는 정상적으로 생성되었다는 것이다.</p>
<p>따라서 다음 단계에서는 실제 영상 스트림을 확인하기 위해 libcamera 테스트 도구와 ffmpeg를 이용해 카메라 동작을 검증한다.</p>
<pre><code class="language-bash">cam -c1 --capture=10</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c88cfee1-1c47-42fc-b833-ad5459d5269f/image.png" alt=""></p>
<p>이 로그는 카메라 스트림이 실제로 동작하고 있음을 증명한다. 다음 단계에서는 일반적인 멀티미디어 도구가 이 카메라 스트림을 사용할 수 있는지 확인하기 위해 ffmpeg 기반 테스트를 진행했다.</p>
<pre><code class="language-bash">libcamerify ffplay /dev/video0</code></pre>
<p>이 명령은 libcamera의 V4L2 호환 계층을 통해 <code>/dev/video0</code> 장치를 ffmpeg에서 사용할 수 있도록 시도한다. 즉 내부적으로 다음과 같은 경로로 동작한다.</p>
<pre><code>ffplay
→ V4L2 인터페이스
→ libcamera V4L2 compatibility layer
→ libcamera pipeline
→ 센서 스트림</code></pre><p><img src="https://velog.velcdn.com/images/dev_hammy/post/ad907552-71bc-4a68-b0b7-9824d1d0b2b2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/f85a4475-087b-4564-85fb-064513961c41/image.png" alt=""></p>
<p>실행 결과 로그에는 다음과 같은 메시지가 나타난다.</p>
<pre><code>V4L2Compat: Camera does not support FrameDurationLimits
Cannot find a proper format for codec &#39;none&#39;
pixel format &#39;none&#39;</code></pre><p>이 메시지는 ffmpeg가 사용할 수 있는 영상 포맷을 찾지 못했다는 의미다. 원인은 센서가 출력하는 원본 데이터 포맷에 있다.</p>
<p>현재 <code>cam</code> 실행 로그에서 확인한 스트림 포맷은 다음과 같다.</p>
<pre><code>1928x1092-SRGGB10</code></pre><p>여기서 <code>SRGGB10</code>은 다음을 의미한다.</p>
<ul>
<li>Bayer 패턴 기반 RAW 데이터</li>
<li>10bit 픽셀 깊이</li>
<li>ISP 처리가 되지 않은 센서 원본 이미지</li>
</ul>
<p>즉 이 카메라는 RGB나 YUV 같은 일반적인 영상 포맷을 직접 출력하는 것이 아니라, 다음과 같은 형태의 데이터를 전달한다.</p>
<pre><code>10bit Bayer RAW (SRGGB10)</code></pre><p>일반적인 웹캠은 보통 다음과 같은 포맷을 제공한다.</p>
<pre><code>YUYV
MJPEG
NV12</code></pre><p>이 포맷들은 대부분의 영상 프로그램에서 바로 처리할 수 있다. 그러나 Bayer RAW 데이터는 색 보정, 디베이어링(demosaic), 화이트밸런스 등의 ISP 처리를 거쳐야 정상적인 영상으로 변환된다.</p>
<p>libcamera는 이러한 처리를 담당하는 IPA(Image Processing Algorithm) 또는 ISP 경로를 통해 RAW 데이터를 영상 포맷으로 변환할 수 있다. 하지만 ffmpeg가 <code>/dev/video0</code>을 통해 직접 접근하는 경우에는 이 처리 단계가 적용되지 않는다.</p>
<h3 id="4-3-디스코드-실행-확인---연결-실패">4-3. 디스코드 실행 확인 - 연결 실패</h3>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/bdd2d093-e78c-4b6d-9393-43af91289567/image.png" alt=""></p>
<p>디스코드 실행시 장치 이름이 <code>ipu6</code>로 보이고 여전히 카메라 연결에 실패하고 있다. </p>
<p>Discord는 libcamera를 직접 사용하는 프로그램이 아니다. Linux 환경에서 Discord는 일반적으로 다음 경로를 통해 카메라를 사용한다.</p>
<pre><code>Discord
→ PipeWire
→ libcamera plugin
→ IPU6</code></pre><p>즉 Discord가 카메라에 접근하려면 PipeWire가 libcamera 카메라를 인식해야 한다. 따라서 Discord에서 “카메라 연결 실패”가 발생하는 경우 대부분 문제는 PipeWire ↔ libcamera 브리지 계층에서 발생한다.</p>
<h4 id="1-pipewire에-libcamera-플러그인이-있는지-확인">(1) PipeWire에 libcamera 플러그인이 있는지 확인</h4>
<p>IPU6 카메라를 PipeWire가 인식하려면 다음 플러그인이 필요하다.</p>
<pre><code>libspa-0.2-libcamera</code></pre><p>이 플러그인은 PipeWire가 libcamera 기반 카메라 장치를 PipeWire 노드로 변환하는 역할을 한다. 설치 여부는 다음 명령으로 확인할 수 있다.</p>
<pre><code class="language-bash">dpkg -l | grep libspa</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/2fd5c8cc-85d6-4710-8c51-71d6f8692da8/image.png" alt=""></p>
<p>정상적인 경우 해당 패키지가 목록에 포함되어 있어야 한다. 만약 없다면 다음 명령으로 설치한다.</p>
<pre><code class="language-bash">sudo apt install libspa-0.2-libcamera</code></pre>
<p>설치 후 PipeWire를 재시작한다.</p>
<pre><code class="language-bash">systemctl --user restart pipewire pipewire-pulse wireplumber</code></pre>
<p>재부팅하여 아래 명령을 실행한다.</p>
<pre><code class="language-bash">pw-cli ls Node</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/08fb4d17-a230-4f71-b86f-e5d1e6dbb87e/image.png" alt=""></p>
<p>PipeWire는 libcamera 카메라가 아니라 V4L2 장치를 카메라로 사용하고 있다. 이 구조를 정리하면 현재 시스템의 카메라 경로는 다음과 같다.</p>
<pre><code>sensor
→ IPU6
→ libcamera
→ V4L2 compatibility layer
→ /dev/video0
→ PipeWire
→ Discord</code></pre><p>여기서 <code>/dev/video0</code> 장치는 실제 하드웨어 장치가 아니라 libcamera의 V4L2 compatibility layer가 생성한 가상 장치이다. libcamera는 다음 모듈을 통해 이 장치를 생성한다.</p>
<pre><code>/usr/local/libexec/libcamera/v4l2-compat.so</code></pre><p>이 모듈의 역할은 libcamera 기반 카메라를 일반적인 V4L2 장치처럼 보이도록 변환하는 것이다. 덕분에 ffmpeg, PipeWire, 브라우저, Discord 같은 프로그램들이 <code>/dev/video0</code> 장치를 카메라처럼 사용할 수 있게 된다.</p>
<h4 id="2-pipewire가-usrlocal-libcamera를-찾는지-확인">(2) PipeWire가 <code>/usr/local</code> libcamera를 찾는지 확인</h4>
<p>이번 과정에서는 libcamera를 직접 빌드하여 다음 경로에 설치했다.</p>
<pre><code>/usr/local/lib/x86_64-linux-gnu</code></pre><p>하지만 일부 PipeWire 환경에서는 기본적으로 다음 경로만 검색한다.</p>
<pre><code>/usr/lib/x86_64-linux-gnu</code></pre><p>이 경우 PipeWire가 libcamera 라이브러리를 로드하지 못하는 상황이 발생할 수 있다. 먼저 시스템 라이브러리 로더에 등록되었는지 확인한다.</p>
<pre><code class="language-bash">ldconfig -p | grep libcamera</code></pre>
<p>이미 이전 단계에서 <code>/usr/local/lib/x86_64-linux-gnu/libcamera.so.0.4</code> 가 등록된 것을 확인했지만, PipeWire가 여전히 찾지 못하는 경우에는 IPA 모듈 경로를 직접 지정할 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/13b23fe7-13a9-468c-86b7-6fc7f4f2aff8/image.png" alt=""></p>
<p>첨부한 스크린샷 결과를 보면 한 가지 중요한 사실을 확인할 수 있다.</p>
<pre><code>libcamera.so.0.4  → /usr/local/lib/x86_64-linux-gnu
libcamera.so.0.2  → /lib/x86_64-linux-gnu</code></pre><p>즉 현재 시스템에는 두 개의 libcamera 버전이 동시에 존재한다.</p>
<ul>
<li>Ubuntu 패키지 버전: <code>libcamera 0.2</code></li>
<li>직접 빌드한 버전: <code>libcamera 0.4</code></li>
</ul>
<p>Linux에서는 보통 <code>/usr/local</code> 경로가 <code>/usr</code>보다 우선순위가 높기 때문에, 직접 빌드한 프로그램들은 다음 라이브러리를 사용하게 된다.</p>
<pre><code>/usr/local/lib/x86_64-linux-gnu/libcamera.so.0.4</code></pre><p>하지만 PipeWire 플러그인의 경우 상황이 조금 다르다. Ubuntu에서 제공되는 PipeWire의 libcamera 플러그인은 보통 배포판에 포함된 libcamera 버전을 기준으로 빌드되어 있다.</p>
<p>즉 내부적으로 다음 라이브러리를 기대하는 경우가 많다.</p>
<pre><code>/lib/x86_64-linux-gnu/libcamera.so.0.2</code></pre><p>반면 현재 시스템에서 실제 카메라 스택은 다음 구조를 사용하고 있다.</p>
<pre><code>libcamera 0.4
→ /usr/local/lib/x86_64-linux-gnu</code></pre><p>이 경우 PipeWire 플러그인이 직접 빌드한 libcamera를 제대로 로드하지 못하면 다음과 같은 상황이 발생할 수 있다.</p>
<pre><code>PipeWire
→ libcamera plugin 로드 실패
→ V4L2 장치 fallback</code></pre><p>실제로 앞 단계에서 <code>pw-cli ls Node</code> 결과를 보면 PipeWire가 libcamera 카메라가 아니라 V4L2 장치를 카메라로 사용하고 있는 것을 확인할 수 있었다.</p>
<pre><code>object.path = &quot;v4l2:/dev/video0&quot;
node.description = &quot;ipu6 (V4L2)&quot;</code></pre><p>이는 PipeWire가 libcamera 카메라 노드를 직접 생성하지 못하고, 대신 <code>/dev/video0</code> 장치를 통해 카메라에 접근하고 있음을 의미한다.</p>
<p>이 문제를 확인하기 위해 libcamera IPA 모듈 경로를 직접 지정해 보았다.</p>
<pre><code class="language-bash">export LIBCAMERA_IPA_MODULE_PATH=/usr/local/lib/x86_64-linux-gnu/libcamera
systemctl --user import-environment LIBCAMERA_IPA_MODULE_PATH
systemctl --user restart wireplumber pipewire pipewire-pulse</code></pre>
<p>이후 환경 변수가 실제로 반영되었는지 확인했다.</p>
<pre><code class="language-bash">printenv LIBCAMERA_IPA_MODULE_PATH</code></pre>
<p>또한 PipeWire와 WirePlumber 로그에 libcamera 관련 메시지가 남는지 확인했다.</p>
<pre><code class="language-bash">journalctl --user -u wireplumber -u pipewire --since &quot;5 min ago&quot; | grep -iE &#39;libcamera|spa|camera|v4l2&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/8d1636af-5031-49f2-b81f-0d451156d03b/image.png" alt=""></p>
<p>여기서 확인되는 핵심은 두 가지다.</p>
<p>첫째, <code>LIBCAMERA_IPA_MODULE_PATH</code> 자체는 정상적으로 설정되었다.
즉 <code>/usr/local/lib/x86_64-linux-gnu/libcamera</code> 경로에 설치된 IPA 모듈은 현재 셸 기준으로는 참조 가능한 상태다.</p>
<p>둘째, PipeWire와 WirePlumber 로그에는 libcamera 관련 메시지가 나타나지 않았다.</p>
<p>이 점이 중요하다. 만약 PipeWire가 직접 빌드한 libcamera 0.4 기반 플러그인을 실제로 로드하고 있었다면, 최소한 libcamera 관련 초기화 흔적이나 SPA 플러그인 로드 메시지가 로그에 남는 편이 자연스럽다. 그런데 관련 로그가 비어 있다는 것은, 현재 PipeWire가 libcamera 경로를 적극적으로 타고 있다기보다 이미 생성된 V4L2 장치를 camera source로 취급하고 있을 가능성이 더 높다는 뜻이다.</p>
<h2 id="5-기존-접근의-한계와-대안-탐색">5. 기존 접근의 한계와 대안 탐색</h2>
<p>4장까지의 작업을 통해 커널 드라이버, media graph, libcamera userspace 스택까지는 상당 부분 복구되었다. 실제로 cam 명령으로 프레임 캡처가 가능했고, PipeWire에서도 카메라 source 자체가 완전히 사라진 상태는 아니었다. 그럼에도 Discord에서는 여전히 카메라 연결 실패가 발생했다.</p>
<p>즉 이 시점의 문제는 더 이상 “센서가 인식되지 않는다”거나 “libcamera가 전혀 동작하지 않는다”는 초기 단계의 문제가 아니었다. 오히려 다음과 같은 복합적인 접합 문제에 가까웠다.</p>
<ul>
<li>커널 단계에서 센서 probe가 성립해야 하고</li>
<li>userspace에서는 최신 libcamera가 필요하며</li>
<li>PipeWire는 그 libcamera를 실제로 소비할 수 있어야 하고</li>
<li>브라우저류 앱과 V4L2 기반 앱은 서로 다른 경로를 통해 카메라를 사용해야 한다</li>
</ul>
<p>이처럼 문제 지점이 한 계층이 아니라 여러 계층에 걸쳐 있었기 때문에, 단순한 패키지 재설치나 환경 변수 조정만으로는 최종 사용자 앱까지 일관되게 동작시키기 어려웠다. 이 시점에서 비슷한 하드웨어 조합에 대해 이미 정리된 해결 절차가 있는지 찾아보게 되었다.</p>
<h3 id="5-1-발견한-설치-스크립트의-성격">5-1. 발견한 설치 스크립트의 성격</h3>
<p>찾아낸 <code>webcam-fix-libcamera/install.sh</code>는 지금까지 수동으로 확인했던 문제들을 전부 하나의 순서로 엮어 자동화한 형태에 가깝다. 스크립트가 전제하는 기본 파이프라인은 다음과 같다.</p>
<pre><code>IVSC → OV02C10 → IPU6 ISYS → libcamera SimplePipeline → PipeWire</code></pre><p>그리고 PipeWire를 직접 쓰지 않는 애플리케이션을 위해 별도의 relay 경로를 둔다.</p>
<pre><code>libcamera → camera-relay → v4l2loopback → /dev/videoX → non-PipeWire 앱</code></pre><p>즉 이 스크립트는 “카메라를 보이게 만드는 것”만이 아니라, 최종적으로 브라우저·Discord·Zoom·OBS 같은 앱에서 실제로 사용할 수 있는 카메라 장치를 제공하는 것을 목표로 한다.</p>
<h3 id="5-2-이-스크립트가-자동화하는-문제해결-단계">5-2. 이 스크립트가 자동화하는 문제해결 단계</h3>
<p>스크립트의 내용을 보면, 수행하는 작업은 크게 여섯 부류로 나눌 수 있다.</p>
<h4 id="1-하드웨어와-펌웨어-전제-조건-점검">(1) 하드웨어와 펌웨어 전제 조건 점검</h4>
<p>스크립트는 먼저 현재 시스템이 실제로 지원 대상인지부터 확인한다.
IPU6 세대(Meteor Lake / Raptor Lake), OV02C10 ACPI 센서 존재 여부, IVSC 펌웨어 파일 존재 여부를 점검한다.</p>
<p>이 단계는 단순한 친절 기능이 아니라 중요하다.
왜냐하면 지금까지의 디버깅 과정에서도 확인했듯, IPU6 카메라 문제는 “웹캠이 안 된다”는 표면 증상은 같아도 실제 원인이 하드웨어 식별 실패, 센서 ACPI 누락, 펌웨어 부재 등으로 완전히 달라질 수 있기 때문이다.</p>
<h4 id="2-26mhz-external-clock-문제-감지-및-dkms-패치-적용">(2) 26MHz external clock 문제 감지 및 DKMS 패치 적용</h4>
<p>이 스크립트에서 가장 눈에 띄는 부분 중 하나는, <code>dmesg</code>에서 다음 오류를 직접 감지한다는 점이다.</p>
<pre><code>external clock 26000000 is not supported</code></pre><p>즉 사용자가 별도로 커널 로그를 뒤져 원인을 추론하지 않아도, 스크립트가 이미 known issue로 인식하고 DKMS 패치 설치를 제안한다.</p>
<p>이 부분은 앞서 수동으로 진행했던 작업과 정확히 대응된다.
즉 우리가 직접 <code>ov02c10.c</code>를 수정하고 DKMS로 배포했던 26MHz 허용 패치가, 이 스크립트에서는 하나의 분기 처리로 포함되어 있다.</p>
<p>다시 말해 이 스크립트는 “IPU6 카메라가 안 된다”는 현상을 막연히 다루는 것이 아니라, Book3/Book4 계열에서 실제로 반복되는 <code>ov02c10</code> external clock mismatch 문제를 이미 별도의 해결 단계로 분리해 두고 있다.</p>
<h4 id="3-ivsc-모듈-로딩-순서와-initramfs-반영">(3) IVSC 모듈 로딩 순서와 initramfs 반영</h4>
<p>스크립트는 <code>mei-vsc</code>, <code>mei-vsc-hw</code>, <code>ivsc-ace</code>, <code>ivsc-csi</code> 모듈을 로드하고, 이들이 부팅 시점부터 먼저 올라오도록 설정한다.
또한 <code>softdep ov02c10 pre: ...</code> 규칙을 추가해, 센서가 probe되기 전에 IVSC 계열 모듈이 준비되도록 만든다.</p>
<p>이 조치는 매우 실전적이다.
센서 드라이버가 코드상으로는 맞아도, 부팅 시점에 필요한 하위 모듈이 아직 준비되지 않았으면 probe가 실패하거나 defer가 누적될 수 있다. 즉 이 단계는 단순 로딩이 아니라 부팅 타이밍 경쟁 조건을 줄이는 작업이다.</p>
<p>이 부분은 수동 디버깅 과정에서는 놓치기 쉬운 지점인데, 스크립트는 이 문제까지 고려하고 있다.</p>
<h4 id="4-ubuntu에서-최신-libcamera를-직접-빌드">(4) Ubuntu에서 최신 libcamera를 직접 빌드</h4>
<p>스크립트는 Ubuntu 계열에서 설치된 libcamera 버전을 확인한 뒤, 최소 요구 버전보다 낮으면 직접 소스 빌드를 수행한다.
특히 여기서 흥미로운 점은 단순히 최신 버전을 빌드하는 데서 끝나지 않고, <code>OV02C10 sensor helper</code> 패치까지 적용한다는 점이다.</p>
<p>즉 단순히 “0.X 이상이면 된다”가 아니라, OV02C10 센서에 대해 auto exposure / gain이 제대로 동작하지 않아 이미지가 지나치게 어두워지는 문제까지 염두에 두고 있다.</p>
<p>또한 빌드 옵션도 다음처럼 명확하다.</p>
<ul>
<li><code>simple</code> pipeline 사용</li>
<li><code>simple</code> IPA 사용</li>
<li><code>gstreamer</code> 활성화</li>
<li><code>v4l2</code> emulation 활성화</li>
</ul>
<p>즉 이 스크립트는 IPU6 전용 proprietary pipeline이 아니라, open-source libcamera SimplePipeline을 명시적으로 채택하고 있다.</p>
<h4 id="5-pipewire-libcamera-spa-plugin-재빌드">(5) PipeWire libcamera SPA plugin 재빌드</h4>
<p>스크립트에서 가장 중요한 부분 중 하나는 Ubuntu에서 PipeWire의 libcamera SPA plugin을 직접 다시 빌드하는 로직이다.</p>
<p>이 부분이 중요한 이유는 앞 단계에서 이미 드러난 문제와 맞닿아 있기 때문이다.
즉 직접 빌드한 libcamera는 <code>/usr/local</code> 아래의 최신 버전을 사용하지만, Ubuntu 기본 PipeWire plugin은 배포판 libcamera를 기준으로 빌드되어 있기 때문에 ABI가 어긋날 수 있다.</p>
<p>스크립트는 이 문제를 알고 있으며, 다음과 같은 판단을 수행한다.</p>
<ul>
<li>현재 시스템의 libcamera 버전 확인</li>
<li>설치된 SPA plugin이 어떤 libcamera에 링크되는지 확인</li>
<li>버전이 낮거나 어긋나 있으면 PipeWire 소스에서 <code>libspa-libcamera.so</code>를 다시 빌드</li>
<li>기존 시스템 경로의 SPA plugin을 교체</li>
</ul>
<p>즉 우리가 이미 한 차례 겪었던 “libcamera 0.4는 잘 돌아가는데 PipeWire는 여전히 native node를 못 띄우는 문제”를, 이 스크립트는 아예 해결 대상의 핵심으로 보고 있다.</p>
<h4 id="6-raw-ipu6-노드-숨김과-relay-경로-제공">(6) raw IPU6 노드 숨김과 relay 경로 제공</h4>
<p>스크립트는 raw <code>Intel IPU6 ISYS Capture*</code> 노드를 udev 규칙과 WirePlumber 설정으로 숨긴다.
이 조치의 목적은 명확하다. 사용자는 실제로 사용할 수 없는 수십 개의 raw node 대신, 최종적으로 의미 있는 카메라 장치만 보게 해야 한다.</p>
<p>또한 PipeWire-native 앱이 아닌 프로그램을 위해 <code>camera-relay</code>와 <code>v4l2loopback</code>을 설치한다.
이 relay는 상시 무겁게 동작하는 것이 아니라, 앱이 장치를 열었을 때만 GStreamer 파이프라인을 시작하는 on-demand 방식이다.</p>
<p>즉 여기서 relay는 임시 우회책이 아니라, non-PipeWire 앱 호환성을 위한 별도 사용자 공간 장치 제공 계층으로 구현된다.</p>
<h3 id="5-3-왜-이-스크립트가-개연성-있는-대안이었는가">5-3. 왜 이 스크립트가 개연성 있는 대안이었는가</h3>
<p>여기까지의 수동 작업만으로도 이미 많은 문제를 해결했다.
그러나 남아 있던 마지막 문제는 단일 계층의 오류가 아니었다.</p>
<ul>
<li>센서 드라이버는 패치되어야 했고</li>
<li>IVSC는 먼저 준비되어야 했으며</li>
<li>libcamera는 최신 버전이어야 했고</li>
<li>PipeWire는 그 libcamera에 맞게 다시 빌드되어야 했으며</li>
<li>앱 종류에 따라 PipeWire native 경로와 V4L2 relay 경로를 나눠야 했다</li>
</ul>
<p>즉 마지막 단계는 “어느 패키지 하나를 더 설치하면 끝나는 문제”가 아니라, 서로 다른 계층의 조합을 올바른 순서로 맞추는 문제였다.</p>
<p>이 점에서 이 스크립트는 설득력이 있었다.
단순한 팁 모음이 아니라, 지금까지 직접 확인했던 병목들을 실제 실행 순서로 연결해 놓았기 때문이다.</p>
<h2 id="6-환경-리셋-및-설치-스크립트-실행">6. 환경 리셋 및 설치 스크립트 실행</h2>
<p>스크립트의 내용이 제법 많은 과정을 담고 있었기에 아예 우분투를 재설치하고 다음 스크립트를 실행해보기로 했다. </p>
<h3 id="6-1-스크립트-실행-후-재부팅">6-1. 스크립트 실행 후 재부팅</h3>
<p>다음 명령어를 실행한다.</p>
<pre><code class="language-bash">curl -sL https://github.com/Andycodeman/samsung-galaxy-book4-linux-fixes/archive/refs/heads/main.tar.gz | tar xz &amp;&amp; cd samsung-galaxy-book4-linux-fixes-main/webcam-fix-libcamera &amp;&amp; ./install.sh &amp;&amp; sudo reboot</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/62a3afe8-2056-4c80-86a5-553ae954d922/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/2a59a2f7-957d-4917-81d5-d8db7b8e06ae/image.png" alt=""></p>
<p>설치 스크립트는 커널 모듈, libcamera, PipeWire SPA 플러그인까지 한 번에 구성하도록 작성되어 있었지만, Ubuntu 기본 개발 환경이 충분하지 않은 경우 PipeWire 플러그인 재빌드 단계에서 멈출 수 있었다. 실제로 이번 실행에서는 <code>dbus-1</code> 의존성을 찾지 못해 Meson 설정 단계가 실패했다. 로그를 보면 <code>libsystemd</code>도 함께 누락되어 있었기 때문에, Ubuntu에서 <code>libdbus-1-dev</code>, <code>libsystemd-dev</code> 패키지를 추가로 설치한 뒤 스크립트를 다시 실행하는 방식으로 진행했다. </p>
<pre><code class="language-bash">sudo apt update
sudo apt install -y libdbus-1-dev libsystemd-dev
cd ~/samsung-galaxy-book4-linux-fixes-main/webcam-fix-libcamera
./install.sh</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/963f218d-9fa9-4c39-bac8-6783228586e0/image.png" alt=""></p>
<p>설치가 완료되었다는 메시지를 확인했다면 재부팅을 수행한다.</p>
<pre><code class="language-bash">sudo reboot</code></pre>
<h3 id="6-2-재부팅-후-검증">6-2. 재부팅 후 검증</h3>
<pre><code class="language-bash">dmesg | grep -iE &#39;ov02c10|OVTI02C1|ipu6|ivsc|external clock|probe|error|failed&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/6bfe9e4f-6ac8-455c-8595-88339efda985/image.png" alt=""></p>
<p>초기에 발견했던 문제가 다시 발생하고 있다. <code>webcam-fix-libcamera</code> repository 내부에 dkms 패치가 있는지 확인해본다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/dc251ba3-b853-4c05-89bf-cbcb729f15bf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/eea5a0d1-9d57-4d89-aaeb-8ebd076ebf4c/image.png" alt=""></p>
<p>레포 구조를 살펴보니 센서 패치는 별도 스크립트였음이 드러났다.</p>
<pre><code class="language-bash">cd ~/samsung-galaxy-book4-linux-fixes-main/ov02c10-26mhz-fix
sudo ./install.sh</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/fdce29cf-4d83-47b4-9226-b6df86f44094/image.png" alt=""></p>
<p>다음으로 <code>No IPA</code> 문제를 점검해본다.</p>
<pre><code class="language-bash">cam -l

ls /usr/local/lib/x86_64-linux-gnu/libcamera

ls /usr/local/lib/x86_64-linux-gnu/libcamera/ipu
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/6b061e62-56c2-4b8c-a3bd-544fedd62320/image.png" alt=""></p>
<p>ipa 파일은 있지만 IPAManger가 엉뚱한 곳에서 ipa 파일을 찾고 있다.</p>
<pre><code class="language-bash">ls -l /usr/local/lib/x86_64-linux-gnu/libcamera/ipa</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/5bc5f279-1f80-4753-be05-46cac29cac14/image.png" alt=""></p>
<pre><code class="language-bash">ldd $(which cam) | grep libcamera</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/4e477c02-5765-438f-a8fc-b511994e6644/image.png" alt=""></p>
<pre><code class="language-bash">export LIBCAMERA_IPA_PATH=/usr/local/lib/x86_64-linux-gnu/libcamera/ipa

LIBCAMERA_LOG_LEVELS=*:DEBUG \
LIBCAMERA_IPA_PATH=/usr/local/lib/x86_64-linux-gnu/libcamera/ipa \
cam -l</code></pre>
<pre><code>madhamster@treadmill:~$ LIBCAMERA_LOG_LEVELS=*:DEBUG \
LIBCAMERA_IPA_PATH=/usr/local/lib/x86_64-linux-gnu/libcamera/ipa \
cam -l
[0:26:04.215679383] [9836]  WARN IPAManager ipa_manager.cpp:134 No IPA found in &#39;/usr/local/lib/x86_64-linux-gnu/libcamera&#39;
[0:26:04.215880734] [9836] DEBUG IPAModule ipa_module.cpp:333 ipa_soft_simple.so: IPA module /usr/local/lib/x86_64-linux-gnu/libcamera/ipa/ipa_soft_simple.so is signed
[0:26:04.215923401] [9836] DEBUG IPAManager ipa_manager.cpp:239 Loaded IPA module &#39;/usr/local/lib/x86_64-linux-gnu/libcamera/ipa/ipa_soft_simple.so&#39;
[0:26:04.215992387] [9836]  INFO Camera camera_manager.cpp:340 libcamera v0.7.0
[0:26:04.216352551] [9837] DEBUG Camera camera_manager.cpp:74 Starting camera manager
[0:26:04.226103265] [9837] DEBUG DeviceEnumerator device_enumerator.cpp:267 New media device &quot;intel-ipu6&quot; created from /dev/media0
[0:26:04.226464200] [9837] DEBUG DeviceEnumerator device_enumerator_udev.cpp:111 Defer media device /dev/media0 due to 49 missing dependencies
[0:26:04.228741840] [9837] DEBUG DeviceEnumerator device_enumerator_udev.cpp:346 All dependencies for media device /dev/media0 found
[0:26:04.228761966] [9837] DEBUG DeviceEnumerator device_enumerator.cpp:295 Added device /dev/media0: intel-ipu6
[0:26:04.229235758] [9837] DEBUG Camera camera_manager.cpp:143 Found registered pipeline handler &#39;simple&#39;
[0:26:04.229385856] [9837] DEBUG DeviceEnumerator device_enumerator.cpp:355 Successful match for media device &quot;intel-ipu6&quot;
[0:26:04.229474145] [9837] DEBUG SimplePipeline simple.cpp:1903 Sensor found for /dev/media0
[0:26:04.230039384] [9837] DEBUG SimplePipeline simple.cpp:501 Found capture device Intel IPU6 ISYS Capture 32
[0:26:04.230082122] [9837] DEBUG CameraSensor camera_sensor_raw.cpp:213 ov02c10 3-0036: unsupported number of sinks (0) or sources (1)
[0:26:04.230180436] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Exposure (0x00980911)
[0:26:04.230335725] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Horizontal Flip (0x00980914)
[0:26:04.230361154] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Vertical Flip (0x00980915)
[0:26:04.230371393] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Camera Orientation (0x009a0922)
[0:26:04.230409094] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Camera Sensor Rotation (0x009a0923)
[0:26:04.230416737] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Vertical Blanking (0x009e0901)
[0:26:04.230423308] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Horizontal Blanking (0x009e0902)
[0:26:04.230430306] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Analogue Gain (0x009e0903)
[0:26:04.230436869] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Link Frequency (0x009f0901)
[0:26:04.230445370] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Pixel Rate (0x009f0902)
[0:26:04.230452440] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Test Pattern (0x009f0903)
[0:26:04.230462016] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Digital Gain (0x009f0905)
[0:26:04.230731454] [9837] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 2 on pad 0/0: Inappropriate ioctl for device
[0:26:04.230743271] [9837]  WARN CameraSensor camera_sensor_legacy.cpp:402 &#39;ov02c10 3-0036&#39;: The PixelArraySize property has been defaulted to 1928x1092
[0:26:04.230748324] [9837] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 1 on pad 0/0: Inappropriate ioctl for device
[0:26:04.230753084] [9837]  WARN CameraSensor camera_sensor_legacy.cpp:413 &#39;ov02c10 3-0036&#39;: The PixelArrayActiveAreas property has been defaulted to (0, 0)/1928x1092
[0:26:04.230761599] [9837] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 0 on pad 0/0: Inappropriate ioctl for device
[0:26:04.230764757] [9837]  WARN CameraSensor camera_sensor_legacy.cpp:421 &#39;ov02c10 3-0036&#39;: Failed to retrieve the sensor crop rectangle
[0:26:04.230767084] [9837]  WARN CameraSensor camera_sensor_legacy.cpp:427 &#39;ov02c10 3-0036&#39;: The sensor kernel driver needs to be fixed
[0:26:04.230770286] [9837]  WARN CameraSensor camera_sensor_legacy.cpp:429 &#39;ov02c10 3-0036&#39;: See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information
[0:26:04.231657303] [9837]  WARN CameraSensorProperties camera_sensor_properties.cpp:538 No static properties available for &#39;ov02c10&#39;
[0:26:04.231667915] [9837]  WARN CameraSensorProperties camera_sensor_properties.cpp:540 Please consider updating the camera sensor properties database
[0:26:04.231714434] [9837] DEBUG CameraSensor camera_sensor.cpp:476 Entity &#39;ov02c10 3-0036&#39; matched by CameraSensorLegacy
[0:26:04.231750259] [9837]  WARN CameraSensor camera_sensor_legacy.cpp:502 &#39;ov02c10 3-0036&#39;: No sensor delays found in static properties. Assuming unverified defaults.
[0:26:04.231833773] [9837] DEBUG DelayedControls delayed_controls.cpp:99 Set a delay of 2 and priority write flag 0 for Exposure
[0:26:04.231846440] [9837] DEBUG DelayedControls delayed_controls.cpp:99 Set a delay of 1 and priority write flag 0 for Analogue Gain
[0:26:04.231925401] [9837] DEBUG SimplePipeline simple.cpp:575 Found pipeline: [ov02c10 3-0036|0] -&gt; [0|Intel IPU6 CSI2 4|1] -&gt; [0|Intel IPU6 ISYS Capture 32]
[0:26:04.232071970] [9837] DEBUG V4L2 v4l2_videodevice.cpp:633 /dev/video33[12:cap]: Opened device PCI:0000:00:05.0: isys: ipu6
[0:26:04.232254278] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Exposure (0x00980911)
[0:26:04.232277183] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Horizontal Flip (0x00980914)
[0:26:04.232285906] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Vertical Flip (0x00980915)
[0:26:04.232293005] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Camera Orientation (0x009a0922)
[0:26:04.232304402] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Camera Sensor Rotation (0x009a0923)
[0:26:04.232311759] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Vertical Blanking (0x009e0901)
[0:26:04.232318150] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Horizontal Blanking (0x009e0902)
[0:26:04.232324684] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Analogue Gain (0x009e0903)
[0:26:04.232331335] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Link Frequency (0x009f0901)
[0:26:04.232339091] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Pixel Rate (0x009f0902)
[0:26:04.232345897] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Test Pattern (0x009f0903)
[0:26:04.232355830] [9837] DEBUG V4L2 v4l2_device.cpp:742 &#39;ov02c10 3-0036&#39;: Control: Digital Gain (0x009f0905)
[0:26:04.232420944] [9837] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/dma_heap/linux,cma: No such file or directory
[0:26:04.232427699] [9837] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/dma_heap/reserved: No such file or directory
[0:26:04.232431762] [9837] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/dma_heap/system: Permission denied
[0:26:04.232435775] [9837] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/udmabuf: Permission denied
[0:26:04.232438029] [9837] ERROR DmaBufAllocator dma_buf_allocator.cpp:119 Could not open any dma-buf provider
[0:26:04.232453248] [9837] ERROR SoftwareIsp software_isp.cpp:88 Failed to create DmaBufAllocator object
[0:26:04.232471443] [9837]  WARN SimplePipeline simple.cpp:620 Failed to create software ISP, disabling software debayering
[0:26:04.232548347] [9837] DEBUG MediaDevice media_device.cpp:854 /dev/media0[intel-ipu6]: &#39;Intel IPU6 CSI2 4&#39;[1] -&gt; &#39;Intel IPU6 ISYS Capture 32&#39;[0]: 0
[0:26:04.232555583] [9837] DEBUG MediaDevice media_device.cpp:854 /dev/media0[intel-ipu6]: &#39;Intel IPU6 CSI2 4&#39;[1] -&gt; &#39;Intel IPU6 ISYS Capture 32&#39;[0]: 1
[0:26:04.232629676] [9837] DEBUG SimplePipeline simple.cpp:870 Link &#39;ov02c10 3-0036&#39;[0] -&gt; &#39;Intel IPU6 CSI2 4&#39;[0]: configured with format 1928x1092-SGRBG10_1X10/Unset
[0:26:04.232645548] [9837] DEBUG SimplePipeline simple.cpp:870 Link &#39;Intel IPU6 CSI2 4&#39;[1] -&gt; &#39;Intel IPU6 ISYS Capture 32&#39;[0]: configured with format 1928x1092-SGRBG10_1X10/Unset
[0:26:04.232695258] [9837] DEBUG SimplePipeline simple.cpp:716 Adding configuration for 1928x1092 in pixel formats [ BA10, pgAA ]
[0:26:04.232806196] [9837] DEBUG SimplePipeline simple.cpp:674 Using frameStart signal from &#39;Intel IPU6 CSI2 4&#39;
[0:26:04.232937075] [9837]  INFO Camera camera_manager.cpp:223 Adding camera &#39;\_SB_.PC00.LNK0&#39; for pipeline handler simple
[0:26:04.232962765] [9837] DEBUG SimplePipeline simple.cpp:2027 Matched on device: /dev/media0
[0:26:04.232967014] [9837] DEBUG Camera camera_manager.cpp:164 Pipeline handler &quot;simple&quot; matched
Available cameras:
1: Internal front camera (\_SB_.PC00.LNK0)</code></pre><p>상기 로그에서 다음 부분에 주목해본다.</p>
<pre><code>[0:32:39.455346359] [9960] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/dma_heap/linux,cma: No such file or directory
[0:32:39.455353293] [9960] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/dma_heap/reserved: No such file or directory
[0:32:39.455357323] [9960] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/dma_heap/system: Permission denied
[0:32:39.455361091] [9960] DEBUG DmaBufAllocator dma_buf_allocator.cpp:106 Failed to open /dev/udmabuf: Permission denied
[0:32:39.455364017] [9960] ERROR DmaBufAllocator dma_buf_allocator.cpp:119 Could not open any dma-buf provider
[0:32:39.455379012] [9960] ERROR SoftwareIsp software_isp.cpp:88 Failed to create DmaBufAllocator object
[0:32:39.455396594] [9960]  WARN SimplePipeline simple.cpp:620 Failed to create software ISP, disabling software debayering</code></pre><p>권한을 확인해본다</p>
<pre><code class="language-bash">ls -l /dev/dma_heap/system /dev/udmabuf

ls -ld /dev/dma_heap /dev/dma_heap/system</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/548c332f-61cf-48fb-8b96-6625f14e4e36/image.png" alt=""></p>
<pre><code class="language-bash">sudo usermod -aG kvm $USER

sudo chmod 666 /dev/dma_heap/system

sudo reboot

cam -l</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/3f8bdc9f-e100-4d9c-8b45-2e4d2fe96475/image.png" alt=""></p>
<p>로그를 보면 DmaBufAllocator 문제는 해결된 것으로 보인다. 다만 문제는 </p>
<pre><code>Unable to get rectangle 2 on pad 0/0
Unable to get rectangle 1 on pad 0/0
Unable to get rectangle 0 on pad 0/0</code></pre><p>와 같은 오류들 때문에 </p>
<pre><code>&#39;ov02c10 3-0036&#39;: The sensor kernel driver needs to be fixed</code></pre><p>위와 같은 로그가 뜨고 있는 점이다.</p>
<pre><code class="language-bash">cam -c1 --capture=10</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/f81e1e48-9f14-41f4-a9d0-aa2d42c71aaa/image.png" alt=""></p>
<pre><code class="language-bash">v4l2-ctl --list-devices</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c8a873e4-1dde-45f1-9002-8958e5cbe286/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/4c2b8cff-3452-4872-904c-190190bc3bb5/image.png" alt=""></p>
<pre><code class="language-bash">gst-launch-1.0 libcamerasrc ! v4l2sink</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/bb7638f8-3acc-4533-b22b-473e23862ca0/image.png" alt=""></p>
<p>명령어를 실행하자 libcamerasrc를 찾을 수 없다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/3b547aae-d0d7-4823-bc10-500659e3515d/image.png" alt=""></p>
<p>그러나 libcamerasrc는 엄연히 존재하고 있었다. </p>
<pre><code class="language-bash">sudo nano /etc/environment</code></pre>
<p>위 명령어를 실행한 후 아래 내용을 추가한다.</p>
<pre><code class="language-bash">GST_PLUGIN_PATH=&quot;/usr/local/lib/x86_64-linux-gnu/gstreamer-1.0&quot;
LD_LIBRARY_PATH=&quot;/usr/local/lib/x86_64-linux-gnu:/usr/local/lib&quot;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c849ca9a-2143-46c5-980c-58707233753e/image.png" alt=""></p>
<pre><code class="language-bash">madhamster@treadmill:~$ gst-launch-1.0 libcamerasrc ! videoconvert ! v4l2sink device=/dev/video0</code></pre>
<p>이제 위 명령어를 실행하면 아래와 같은 로그가 나온다.</p>
<pre><code>파이프라인을 PAUSED로 만드는 중 ...
[0:17:37.076141490] [4916]  WARN IPAManager ipa_manager.cpp:134 No IPA found in &#39;/usr/local/lib/x86_64-linux-gnu/libcamera&#39;
[0:17:37.076288395] [4916]  INFO Camera camera_manager.cpp:340 libcamera v0.7.0
[0:17:37.088204604] [4918] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 2 on pad 0/0: 장치에 대해 부적절한 ioctl
[0:17:37.088242071] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:402 &#39;ov02c10 3-0036&#39;: The PixelArraySize property has been defaulted to 1928x1092
[0:17:37.088249780] [4918] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 1 on pad 0/0: 장치에 대해 부적절한 ioctl
[0:17:37.088255465] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:413 &#39;ov02c10 3-0036&#39;: The PixelArrayActiveAreas property has been defaulted to (0, 0)/1928x1092
[0:17:37.088261377] [4918] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 0 on pad 0/0: 장치에 대해 부적절한 ioctl
[0:17:37.088266160] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:421 &#39;ov02c10 3-0036&#39;: Failed to retrieve the sensor crop rectangle
[0:17:37.088270463] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:427 &#39;ov02c10 3-0036&#39;: The sensor kernel driver needs to be fixed
[0:17:37.088274293] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:429 &#39;ov02c10 3-0036&#39;: See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information
[0:17:37.089101819] [4918]  WARN CameraSensorProperties camera_sensor_properties.cpp:538 No static properties available for &#39;ov02c10&#39;
[0:17:37.089113329] [4918]  WARN CameraSensorProperties camera_sensor_properties.cpp:540 Please consider updating the camera sensor properties database
[0:17:37.089142515] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:502 &#39;ov02c10 3-0036&#39;: No sensor delays found in static properties. Assuming unverified defaults.
[0:17:37.092556229] [4918]  INFO IPAProxy ipa_proxy.cpp:180 Using tuning file /usr/local/share/libcamera/ipa/simple/ov02c10.yaml
[0:17:37.092583275] [4918] ERROR V4L2 v4l2_subdevice.cpp:1192 &#39;ov02c10 3-0036&#39;: Unable to get rectangle 0 on pad 0/0: 장치에 대해 부적절한 ioctl
[0:17:37.092593324] [4918]  WARN CameraSensor camera_sensor_legacy.cpp:881 &#39;ov02c10 3-0036&#39;: The analogue crop rectangle has been defaulted to the active area size
[0:17:37.092699054] [4918]  WARN IPASoft soft_simple.cpp:104 IPASoft: Failed to create camera sensor helper for ov02c10
[0:17:37.093571546] [4918]  INFO Camera camera_manager.cpp:223 Adding camera &#39;\_SB_.PC00.LNK0&#39; for pipeline handler simple
파이프라인이 살아있으므로 PREROLL이 필요하지 않음 ...
파이프라인이 PREROLL됨...
파이프라인을 재생중으로 변경중 ...
New clock: GstSystemClock
[0:17:37.096714995] [4919]  INFO Camera camera.cpp:1215 configuring streams: (0) 1920x1080-ABGR8888/sRGB
[0:17:37.097182866] [4918]  INFO IPASoft soft_simple.cpp:258 IPASoft: Exposure 4-2320, gain 16-248 (1)
ERROR: from element /GstPipeline:pipeline0/GstV4l2Sink:v4l2sink0: Device &#39;/dev/video0&#39; is busy
Additional debug info:
../sys/v4l2/gstv4l2object.c(4417): gst_v4l2_object_set_format_full (): /GstPipeline:pipeline0/GstV4l2Sink:v4l2sink0:
Call to S_FMT failed for YUYV @ 1920x1080: 장치나 자원이 동작 중
ERROR: from element /GstPipeline:pipeline0/GstV4l2Sink:v4l2sink0: Device &#39;/dev/video0&#39; is busy
Additional debug info:
../sys/v4l2/gstv4l2object.c(4417): gst_v4l2_object_set_format_full (): /GstPipeline:pipeline0/GstV4l2Sink:v4l2sink0:
Call to S_FMT failed for YUYV @ 1920x1080: 장치나 자원이 동작 중
Execution ended after 0:00:00.004549685
파이프라인을 NULL로 만드는 중 ...
[0:17:37.125190562] [4922]  INFO eGL egl.cpp:305 EGL: EGL_VERSION: 1.5
[0:17:37.125226134] [4922]  INFO eGL egl.cpp:306 EGL: EGL_VENDOR: Mesa Project
[0:17:37.125228636] [4922]  INFO eGL egl.cpp:307 EGL: EGL_CLIENT_APIS: OpenGL OpenGL_ES 
[0:17:37.125230761] [4922]  INFO eGL egl.cpp:308 EGL: EGL_EXTENSIONS: EGL_ANDROID_blob_cache EGL_ANDROID_native_fence_sync EGL_EXT_config_select_group EGL_EXT_create_context_robustness EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers EGL_EXT_protected_content EGL_EXT_query_reset_notification_strategy EGL_EXT_surface_compression EGL_IMG_context_priority EGL_KHR_cl_event2 EGL_KHR_config_attribs EGL_KHR_context_flush_control EGL_KHR_create_context EGL_KHR_create_context_no_error EGL_KHR_fence_sync EGL_KHR_get_all_proc_addresses EGL_KHR_gl_colorspace EGL_KHR_gl_renderbuffer_image EGL_KHR_gl_texture_2D_image EGL_KHR_gl_texture_3D_image EGL_KHR_gl_texture_cubemap_image EGL_KHR_image_base EGL_KHR_no_config_context EGL_KHR_partial_update EGL_KHR_reusable_sync EGL_KHR_surfaceless_context EGL_EXT_pixel_format_float EGL_KHR_wait_sync EGL_MESA_configless_context EGL_MESA_gl_interop EGL_MESA_image_dma_buf_export EGL_MESA_query_driver EGL_MESA_x11_native_visual_id 
[0:17:37.127292973] [4922]  INFO eGL egl.cpp:349 EGL: GL_VERSION: OpenGL ES 3.2 Mesa 25.2.8-0ubuntu0.24.04.1
파이프라인 비우는 중 ...</code></pre><p>위 로그에서 주목할만한 부분은 아래와 같다.</p>
<pre><code>ERROR: from element /GstPipeline:pipeline0/GstV4l2Sink:v4l2sink0: Device &#39;/dev/video0&#39; is busy</code></pre><p>누가 <code>/dev/video0</code>을 점유하고 있는지 확인한다</p>
<pre><code class="language-bash">fuser -v /dev/video0

# 또는

lsof /dev/video0</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/acb64aec-73ef-4d0f-9477-bfe5e6da80cb/image.png" alt=""></p>
<pre><code class="language-bash">pkill -f camera-relay

fuser -v /dev/video0</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/d891a1a1-97be-43ea-9e64-0cf763b16faf/image.png" alt=""></p>
<p>점유하고 있는 프로그램이 없음을 확인하고 다시 명령어를 실행한다. </p>
<pre><code class="language-bash">gst-launch-1.0 -v libcamerasrc ! queue ! videoconvert ! queue ! autovideosink</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/d1df3a4d-b633-4240-974d-ca6bbdc7f6a4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/e07ce35c-8299-4c74-96ea-eab022c7bbd9/image.png" alt=""></p>
<pre><code class="language-bash">sudo apt install linux-modules-ipu6-generic-hwe-24.04</code></pre>
<p>재부팅 후 아래 명령어 실행</p>
<pre><code class="language-bash">lsmod | grep ipu6

ls /dev | grep ipu
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/7b88d2e2-0edc-4de0-8559-30cd77f698dd/image.png" alt=""></p>
<pre><code class="language-bash">sudo dmesg | grep -i ipu6</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/e3aced79-4859-460a-9a3e-2da0a8418317/image.png" alt=""></p>
<pre><code class="language-bash">dpkg -l | grep -Ei &#39;ipu6|camhal|libcamhal|camera-hal|camera-bins&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/55cd653c-2597-4f63-81ff-9dc4786f2f70/image.png" alt=""></p>
<pre><code class="language-bash">sudo add-apt-repository ppa:oem-solutions-group/intel-ipu6
sudo apt update</code></pre>
<pre><code class="language-bash">sudo apt install ipu6-camera-bins ipu6-camera-hal libcamhal-ipu6</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/5b12aeb2-1b18-4611-b4f4-3e822320d869/image.png" alt=""></p>
<pre><code class="language-bash">apt search libcamhal
apt search icamera
apt search v4l2-relayd
apt search ipu6 | sed -n &#39;1,120p&#39;</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/3e3a201d-d91d-48cc-a47f-ee2ad675158a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/8e17c5cf-ae46-4134-b1f2-6c61308a45cb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/d68b6495-ad50-4ad6-801a-ae8c327d7bda/image.png" alt=""></p>
<pre><code>madhamster@treadmill:~$ apt search ipu6 | sed -n &#39;1,120p&#39;

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

정렬 중...
전체 텍스트 검색...
gstreamer1.0-icamera/noble 0~git202509260937.4fb31db~ubuntu24.04.7 amd64
  MIPI camera through Intel IPU6 plugin for GStreamer

intel-ipu6-dkms/noble-updates 0~git202406240945.aecec2aa-0ubuntu2~24.04.3 amd64
  Intel Integrated Image Processing Unit 6 (IPU6) driver

intel-usbio-dkms/noble-updates 0~git202312141918.78ffb706-0ubuntu2.2 amd64
  USBIO Bridge drivers for Intel MeteoLake platform with Lattice AIC

libbroxton-ia-pal-dev/noble 0~git202310270317.0dad591-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libbroxton-ia-pal-ipu6-0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libbroxton-ia-pal-ipu6-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libbroxton-ia-pal-ipu60/noble 0~git202410220058.e00b29f-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libbroxton-ia-pal-ipu6ep-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libbroxton-ia-pal-ipu6ep0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libbroxton-ia-pal-ipu6epmtl-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libbroxton-ia-pal-ipu6epmtl0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libbroxton-ia-pal0/noble 0~git202310270317.0dad591-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libcamhal-common/noble 0~git202601200757.9899efa~ubuntu24.04.1 all
  HAL library for MIPI camera through Intel IPU6 - common files

libcamhal-dev/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  HAL library for MIPI camera through Intel IPU6 - development kit

libcamhal-ipu6/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  Dynamic loading plugin for IPU6 camera (ipu6)

libcamhal-ipu6-common/noble 0~git202601200757.9899efa~ubuntu24.04.1 all
  Common files for for MIPI camera through Intel IPU6 (ipu6)

libcamhal-ipu6-dev/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  Dynamic loading plugin for IPU6 camera (ipu6) - development kit

libcamhal-ipu6ep/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  Dynamic loading plugin for IPU6 camera (ipu6ep)

libcamhal-ipu6ep-common/noble 0~git202601200757.9899efa~ubuntu24.04.1 all
  Common files for for MIPI camera through Intel IPU6 (ipu6ep)

libcamhal-ipu6ep-dev/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  Dynamic loading plugin for IPU6 camera (ipu6ep) - development kit

libcamhal-ipu6ep0/noble 0~git202410220058.74ffeab~ubuntu24.04.2 amd64
  Transitional package for libcamhal0

libcamhal-ipu6epmtl/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  Dynamic loading plugin for IPU6 camera (ipu6epmtl)

libcamhal-ipu6epmtl-common/noble 0~git202601200757.9899efa~ubuntu24.04.1 all
  Common files for for MIPI camera through Intel IPU6 (ipu6epmtl)

libcamhal-ipu6epmtl-dev/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  Dynamic loading plugin for IPU6 camera (ipu6epmtl) - development kit

libcamhal0/noble 0~git202601200757.9899efa~ubuntu24.04.1 amd64
  HAL library for MIPI camera through Intel IPU6

libgcss-dev/noble 0~git202310270317.0dad591-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libgcss-ipu6-0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libgcss-ipu6-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libgcss-ipu60/noble 0~git202410220058.e00b29f-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libgcss-ipu6ep-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libgcss-ipu6ep0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libgcss-ipu6epmtl-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libgcss-ipu6epmtl0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libgcss0/noble 0~git202310270317.0dad591-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libgsticamerainterface-1.0-1/noble 0~git202509260937.4fb31db~ubuntu24.04.7 amd64
  GStreamer MIPI camera through Intel IPU6 interface (shared library)

libgsticamerainterface-1.0-dev/noble 0~git202509260937.4fb31db~ubuntu24.04.7 amd64
  GStreamer MIPI camera through Intel IPU6 interface (development files)

libia-aiq-dev/noble 0~git202310270317.0dad591-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libia-aiq-file-debug-dev/noble 0~git202310270317.0dad591-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera (development files)

libia-aiq-file-debug-ipu6-0/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64
  API library for Intel IPU6 camera

libia-aiq-file-debug-ipu6-dev/noble 0~git202506270118.30e8766-1~ubuntu24.04.1 amd64</code></pre><p>위와 같이 정보를 확인하였으면 적당한 것을 설치한다.</p>
<pre><code class="language-bash">sudo apt install libcamhal0 libcamhal-common libcamhal-ipu6 gstreamer1.0-icamera</code></pre>
<p>설치후 재부팅하는 것을 잊지 않는다.</p>
<pre><code class="language-bash">gst-inspect-1.0 icamerasrc</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/a343efab-4323-4342-9ff2-7b193184da68/image.png" alt=""></p>
<p>어딘가 이상한 로그. 아래 명령어를 실행한다.</p>
<pre><code class="language-bash">sudo apt install libcamhal-ipu6epmtl libcamhal-ipu6epmtl-common
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/00e4b536-9be5-4c66-8749-83bf22d13439/image.png" alt=""></p>
<p>실행 후 다시 테스트해본다.</p>
<pre><code>madhamster@treadmill:~$ gst-launch-1.0 icamerasrc ! autovideosink
파이프라인을 PAUSED로 만드는 중 ...
[03-09 01:57:14.313] CamHAL[INF] aiqb file name OV02C10_1BG203N3_ADL.aiqb
[03-09 01:57:14.313] CamHAL[INF] aiqb file name OV02C10_1BG203N3_ADL.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name OV02C10_1SG204N3_ADL.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name OV02C10_1SG204N3_ADL.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name OV02C10_CIFME14_ADL.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name OV02C10_CIFME14_ADL.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.314] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.315] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.315] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.315] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
[03-09 01:57:14.315] CamHAL[INF] aiqb file name AR0234_TGL_10bits.aiqb
파이프라인이 살아있으므로 PREROLL이 필요하지 않음 ...
파이프라인이 PREROLL됨...
파이프라인을 재생중으로 변경중 ...
New clock: GstSystemClock
[03-09 01:57:14.348] CamHAL[ERR] Failed to open PSYS, error: 허가 거부
[03-09 01:57:14.348] CamHAL[ERR] Failed to initialize Context
[03-09 01:57:14.348] CamHAL[ERR] create PG 187 error
[03-09 01:57:14.348] CamHAL[ERR] Failed to create PGs for executor: ipu6_lb_video_bayer
[03-09 01:57:14.348] CamHAL[ERR] Failed to create pipe for executor:ipu6_lb_video_bayer
[03-09 01:57:14.348] CamHAL[ERR] @configure, create psys executors failed
[03-09 01:57:14.348] CamHAL[ERR] @configure configure psys dag failed:-2147483648
[03-09 01:57:14.348] CamHAL[ERR] Configure processor failed with:-2147483648
[03-09 01:57:14.348] CamHAL[ERR] @configure configure post processor failed with:-2147483648
[03-09 01:57:14.348] CamHAL[ERR] failed to config streams.
ERROR: from element /GstPipeline:pipeline0/Gstcamerasrc:camerasrc0: src pad: Internal data flow error.
Additional debug info:
gstcambasesrc.cpp(3156): gst_cam_base_src_loop (): /GstPipeline:pipeline0/Gstcamerasrc:camerasrc0:
streaming task paused, reason not-negotiated (-4)
Execution ended after 0:00:00.029366162
파이프라인을 NULL로 만드는 중 ...
[03-09 01:57:14.348] CamHAL[WAR] Failed to open file /run/camera/ov02c10-uf_VIDEO.aiqd, error 그런 파일이나 디렉터리가 없습니다
파이프라인 비우는 중 ...</code></pre><p>psys 실행을 하지 못하는 권한 문제가 있으므로 아래 명령어를 실행한다</p>
<pre><code class="language-bash">sudo chgrp video /dev/ipu-psys0
sudo chmod 660 /dev/ipu-psys0
sudo usermod -aG video $USER</code></pre>
<pre><code class="language-bash">sudo nano /etc/udev/rules.d/99-ipu6.rules</code></pre>
<p>위 명령어를 실행하며 내용으로는 아래와 같이 쓴다.</p>
<pre><code class="language-bash">KERNEL==&quot;ipu-psys*&quot;, MODE=&quot;0660&quot;, GROUP=&quot;video&quot;
KERNEL==&quot;ipu*&quot;, MODE=&quot;0660&quot;, GROUP=&quot;video&quot;</code></pre>
<p>적용은 아래와 같이 한다.</p>
<pre><code class="language-bash">sudo udevadm control --reload-rules
sudo udevadm trigger</code></pre>
<p>재부팅 후 재실행</p>
<pre><code class="language-bash">gst-launch-1.0 icamerasrc ! autovideosink</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/a9f621e6-ea15-48d7-b587-4437480a9ae4/image.png" alt=""></p>
<p>그럼 뒤집어지긴 했지만 일단은 영상이 나온다.</p>
<pre><code class="language-bash">gst-launch-1.0 icamerasrc ! videoflip method=rotate-180 ! videoconvert ! autovideosink</code></pre>
<p>명령어를 위와 같이 실행하면 180도 회전된 화면으로 나온다</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/1f5b7c51-ed04-4bfa-be1b-4e9ec49df5b6/image.png" alt=""></p>
<p>디스코드에서는 거꾸로인 화면으로 나온다.</p>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/50597602-d7fb-4199-9fb5-1d1934bacb3f/image.png" alt=""></p>
<p>화면 조정 하는 부분은 다른 분께서 해주실 것 같아서 멈추기로 한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Ubuntu] SAMSUNG Galaxy Book 4 Pro 360에 Ubuntu 24.04 설치 시 소리 안나오는 문제 해결 ]]></title>
            <link>https://velog.io/@dev_hammy/Ubuntu-SAMSUNG-Galaxy-Book-4-Pro-360%EC%97%90-Ubuntu-24.04-%EC%84%A4%EC%B9%98-%EC%8B%9C-%EC%86%8C%EB%A6%AC-%EC%95%88%EB%82%98%EC%98%A4%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@dev_hammy/Ubuntu-SAMSUNG-Galaxy-Book-4-Pro-360%EC%97%90-Ubuntu-24.04-%EC%84%A4%EC%B9%98-%EC%8B%9C-%EC%86%8C%EB%A6%AC-%EC%95%88%EB%82%98%EC%98%A4%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Wed, 18 Feb 2026 09:41:44 GMT</pubDate>
            <description><![CDATA[<h2 id="0-빠른-결론">0. 빠른 결론</h2>
<p><a href="https://github.com/Andycodeman/samsung-galaxy-book4-linux-fixes">https://github.com/Andycodeman/samsung-galaxy-book4-linux-fixes</a></p>
<p>위 링크를 이용하세요</p>
<h2 id="1-최초의-인식">1. 최초의 인식</h2>
<h3 id="1-1-1단계-정보">1-1. 1단계 정보</h3>
<ul>
<li>Laptop Model Name - NT960QGK</li>
<li>OS - Ubuntu 24.04</li>
<li>증상 - 이어폰에서는 소리가 나는데 내장 스피커에는 소리가 나지 않음</li>
</ul>
<h3 id="1-2-1단계에서-유추-가능한-원인">1-2. 1단계에서 유추 가능한 원인</h3>
<h4 id="1-가설-a--스피커만-별도-하드웨어-경로를-타는-구조-문제-앰프스피커-경로-미동작">(1) 가설 A : “스피커만” 별도 하드웨어 경로를 타는 구조 문제 (앰프/스피커 경로 미동작)</h4>
<p>이어폰이 정상이라는 것은 오디오 스택과 사운드 카드 자체는 살아 있다는 의미다. 반면 스피커만 무음이라면, 코덱 이후 단계(앰프·라우팅·스피커 드라이버) 중 어딘가에서 경로가 끊겼을 가능성이 높다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>사운드 카드(Sound Card)</strong>
운영체제가 인식하는 오디오 장치의 논리적 단위다. 물리적으로는 코덱, DSP, 앰프 등이 결합된 형태지만, Linux에서는 하나의 오디오 장치(예: <code>sof-hda-dsp</code>, <code>HDA Intel</code>)로 추상화되어 보인다.</li>
</ul>
<ul>
<li><strong>오디오 스택(Audio Stack)</strong>
소리가 출력되기까지 거치는 소프트웨어 계층 전체를 의미한다.
일반적으로 <code>Application → PipeWire(또는 PulseAudio) → ALSA → 커널 드라이버 → 하드웨어</code> 순으로 동작한다. 이 중 어느 한 계층이 정상적으로 연결되지 않으면 출력 문제가 발생할 수 있다.</li>
</ul>
<ul>
<li><strong>코덱(Codec)</strong>
디지털 오디오 신호를 아날로그로 변환(DAC)하거나, 반대로 변환(ADC)하는 칩이다. 이어폰 출력은 보통 코덱을 통해 직접 구동된다.</li>
</ul>
<ul>
<li><strong>앰프(Amplifier)</strong>
코덱에서 나온 약한 아날로그 신호를 스피커를 울릴 수 있는 수준으로 증폭하는 장치다. 최근 노트북은 MAX 계열과 같은 스마트 앰프를 별도로 사용한다.</li>
</ul>
<ul>
<li><strong>스피커 유닛(Speaker Unit)</strong>
전기 신호를 실제 공기 진동으로 변환해 소리를 만들어내는 물리적 부품이다. 코덱과 앰프가 전자적 처리 장치라면, 스피커 유닛은 최종 출력 장치다.</li>
</ul>
<ul>
<li><strong>스피커 라우팅(Speaker Routing)</strong>
오디오 신호가 “DSP → 코덱 → 앰프 → 스피커 유닛”으로 전달되는 전체 경로를 의미한다. 이 경로가 소프트웨어적으로 정확히 설정되어야 실제 출력이 발생한다.</li>
</ul>
<ul>
<li><strong>스피커 드라이버(Driver)</strong>
스피커 유닛이 아니라, 코덱이나 앰프 같은 오디오 칩을 제어하기 위한 커널 모듈을 의미한다. 해당 드라이버가 로딩되지 않으면 앰프가 활성화되지 않고 결과적으로 스피커가 동작하지 않는다.</li>
</ul>
</blockquote>
<h4 id="2-가설-b--오디오-라우팅포트-전환-실패-경로는-정의되어-있는데-선택전환이-잘못된-상태">(2) 가설 B : 오디오 라우팅/포트 전환 실패 (경로는 정의되어 있는데 “선택/전환”이 잘못된 상태)</h4>
<p>스피커 장치가 UI 상에 정상적으로 표시되더라도, 실제 오디오 신호가 해당 포트로 전달되지 않으면 출력은 발생하지 않는다.</p>
<p>예를 들어,</p>
<ul>
<li>스피커 포트가 기본 출력으로 선택되지 않았거나</li>
<li>자동 음소거(auto-mute) 또는 잘못된 jack detect 신호로 인해 스피커가 차단되었거나</li>
<li>활성 프로파일과 실제 포트 매핑이 일치하지 않아 ‘Speaker’를 선택해도 내부 라우팅이 열리지 않는 경우</li>
</ul>
<p>와 같은 상황이 발생할 수 있다.</p>
<p>이 경우 문제는 물리적 하드웨어 결함이 아니라, 오디오 스택 상에서 포트·프로파일·라우팅 로직이 올바르게 연결되지 않은 상태에 가깝다.</p>
<p>즉, “스피커가 존재하지 않는 것”이 아니라, “스피커 경로가 열리지 않은 것”이라는 관점에서 접근해야 한다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>포트(Port)</strong>
하나의 사운드 카드 안에서 실제 물리적 출력 단자를 구분하는 논리적 단위다. 예를 들어 “Speaker”, “Headphones”, “HDMI”는 각각 서로 다른 포트로 관리된다. 포트가 올바르게 선택되지 않으면 원하는 출력 장치로 소리가 전달되지 않는다.</li>
</ul>
<ul>
<li><strong>프로파일(Profile)</strong>
사운드 카드가 어떤 입·출력 구성을 사용할지를 정의한 모드다. 예를 들어 “HiFi”, “Off”, “Pro Audio” 등으로 나뉘며, 각 프로파일은 사용 가능한 포트와 채널 구성을 결정한다. 잘못된 프로파일이 활성화되면 특정 출력 경로가 비활성화될 수 있다.</li>
</ul>
<ul>
<li><strong>Jack Detect</strong>
이어폰 단자에 플러그가 꽂혔는지를 감지하는 하드웨어 신호다. 이 신호에 따라 시스템은 자동으로 스피커를 음소거하거나 출력 포트를 전환한다. Jack detect가 오작동하면 이어폰이 꽂혀 있지 않아도 스피커가 차단될 수 있다.</li>
</ul>
<ul>
<li><strong>오디오 라우팅(Audio Routing)</strong>
애플리케이션에서 생성된 오디오 신호가 어떤 포트를 통해 어떤 하드웨어 경로로 전달될지를 결정하는 과정이다. 올바른 라우팅이 이루어지지 않으면, UI에서 ‘Speaker’를 선택해도 실제 신호는 다른 경로로 향하거나 차단될 수 있다.</li>
</ul>
</blockquote>
<h4 id="3-가설-c--장치는-인식되지만-스피커-경로-정의가-없음-ucm--topology--모델별-설정-데이터-부재">(3) 가설 C : 장치는 인식되지만, 스피커 경로 정의가 없음 (UCM / Topology / 모델별 설정 데이터 부재)</h4>
<p>Linux 환경에서 사운드 카드가 인식된다는 것은 커널 레벨에서 오디오 장치가 바인딩되었음을 의미한다. 그러나 이것만으로 스피커 출력이 보장되는 것은 아니다. 각 노트북 모델마다 스피커, 마이크, 이어폰이 어떤 회로 구성과 신호 경로로 연결되어 있는지에 대한 모델별 정의 데이터가 필요하다.</p>
<p>이 정의는 일반적으로 다음과 같은 형태로 존재한다.</p>
<ul>
<li>UCM(Use Case Manager) 프로파일</li>
<li>SOF Topology(.tplg) 파일</li>
<li>모델별 quirk 또는 초기화 시퀀스</li>
</ul>
<p>이 데이터에는 다음과 같은 정보가 포함된다.</p>
<ul>
<li>어떤 출력 포트를 스피커로 간주할 것인지</li>
<li>스피커 앰프를 어떤 순서로 활성화할 것인지</li>
<li>DSP 내부에서 어떤 노드들을 연결할 것인지</li>
<li>이어폰 삽입 시 어떤 경로를 차단하거나 전환할 것인지</li>
</ul>
<p>해당 모델(NT960QGK)에 대한 정의가 기본 배포판에 포함되어 있지 않다면, 시스템은 장치를 인식하더라도 스피커 경로를 완성하지 못할 수 있다.</p>
<p>이 경우 다음과 같은 현상이 나타날 수 있다.</p>
<ul>
<li>스피커 포트가 UI에 표시됨</li>
<li>볼륨 조절이 가능해 보임</li>
<li>이어폰은 정상 동작</li>
</ul>
<p>그러나 실제로는 스피커 경로에 필요한 앰프 활성화나 내부 라우팅이 수행되지 않아 출력이 발생하지 않는다. 이 상황은 단순한 포트 선택 오류나 음소거 상태와는 다르다. 문제의 원인은 설정 값이 아니라, 해당 모델에 대한 오디오 경로 정의 자체가 부족하거나 누락된 상태에 있다.</p>
<p>따라서 해결 역시 포트 전환이나 믹서 설정 변경이 아니라,</p>
<ul>
<li>모델에 맞는 UCM 업데이트</li>
<li>topology 파일 보완</li>
<li>별도 커널 모듈 또는 앰프 드라이버 추가</li>
</ul>
<p>와 같이 정의 데이터를 보완하는 방향으로 접근해야 한다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>UCM(Use Case Manager)</strong>
ALSA에서 사용하는 모델별 오디오 설정 데이터다. 특정 노트북에서 스피커, 이어폰, 마이크를 사용할 때 어떤 믹서 스위치를 켜고 어떤 포트를 활성화해야 하는지를 정의한다. 쉽게 말해 “이 장치에서 스피커를 정상 동작시키기 위한 설정 시나리오”를 담은 파일이다.</li>
</ul>
<ul>
<li><strong>Topology(.tplg 파일)</strong>
SOF 기반 오디오에서 DSP 내부 신호 흐름을 정의하는 파일이다. DSP 안에는 여러 오디오 처리 블록이 존재하며, topology 파일은 이 블록들을 어떻게 연결할지를 지정한다. 즉, DSP 내부 배선도에 해당한다.</li>
</ul>
<ul>
<li><strong>Quirk(모델별 예외 처리)</strong>
특정 하드웨어 모델에서만 필요한 커널 레벨의 특수 설정을 의미한다. 예를 들어 특정 GPIO를 먼저 활성화해야 하거나, 특정 장치를 강제로 등록해야 하는 경우가 이에 해당한다. 일반적인 드라이버 동작으로는 해결되지 않는 모델별 예외 상황을 처리하는 장치다.</li>
</ul>
<ul>
<li><strong>DSP(Digital Signal Processor)</strong>
오디오 신호를 처리하는 전용 프로세서다. 단순히 소리를 증폭하는 것이 아니라, 믹싱, 필터링, 노이즈 처리, 경로 제어 등을 수행한다. 최신 Intel 플랫폼에서는 SOF가 이 DSP를 제어한다.</li>
</ul>
<ul>
<li><strong>초기화 시퀀스(Initialization Sequence)</strong>
오디오 하드웨어가 정상 동작하기 위해 부팅 시 순차적으로 실행되어야 하는 설정 절차를 의미한다. 예를 들어 앰프 전원을 켜고, 레지스터 값을 설정하고, 출력 경로를 활성화하는 단계들이 포함된다. 이 순서가 누락되거나 잘못되면 장치는 인식되더라도 실제 출력이 발생하지 않는다.</li>
</ul>
</blockquote>
<h4 id="4-가설-d--커널·펌웨어·드라이버-레벨-문제">(4) 가설 D : 커널·펌웨어·드라이버 레벨 문제</h4>
<p>스피커만 무음인 경우에도 커널이나 펌웨어, 드라이버 수준의 문제가 원인일 가능성은 배제할 수 없다. 다만, 이 단계에서는 이를 단정할 근거가 없다. 다음 항목들은 모두 실제 시스템 상태를 확인해야 판단할 수 있는 영역이다.</p>
<ul>
<li>현재 사용 중인 커널 버전이 해당 하드웨어를 충분히 지원하는지</li>
<li>필요한 펌웨어가 정상적으로 로딩되었는지</li>
<li>오디오 드라이버가 장치에 올바르게 바인딩되었는지</li>
<li>사용자 공간 오디오 서버가 무엇인지(PipeWire 또는 PulseAudio)</li>
</ul>
<p>이 요소들은 단순 추정이 아니라, 명령어를 통해 확인해야 하는 기술적 사실에 해당한다. 따라서 본 단계에서는 커널·펌웨어·드라이버 문제를 하나의 가능성으로 열어두되, 실제 검증은 다음 단계에서 수행한다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>커널(Kernel)</strong>
운영체제의 핵심 구성 요소로, 하드웨어와 소프트웨어 사이에서 자원 관리와 장치 제어를 담당한다. 오디오 장치의 경우, 커널 안에 포함된 드라이버가 실제 사운드 카드와 통신한다. 커널 버전이 해당 하드웨어를 지원하지 않으면 장치가 부분적으로만 동작하거나 특정 기능이 누락될 수 있다.</li>
</ul>
<ul>
<li><strong>펌웨어(Firmware)</strong>
하드웨어 내부에서 실행되는 저수준 코드로, 장치가 정상 동작하기 위해 필요한 초기화 및 제어 로직을 포함한다. Linux에서는 커널 드라이버가 부팅 시 필요한 펌웨어 파일을 로드한다. 적절한 펌웨어가 존재하지 않거나 버전이 맞지 않으면 장치 초기화가 완전히 이루어지지 않을 수 있다.</li>
</ul>
<ul>
<li><strong>드라이버(Driver)</strong>
커널 안에서 특정 하드웨어를 제어하는 모듈이다. 오디오의 경우 코덱, DSP, 앰프 등 각 구성 요소에 대한 드라이버가 존재한다. 드라이버가 로딩되지 않았거나 하드웨어와 바인딩되지 않으면 장치는 인식되지 않거나 일부 기능만 동작한다.</li>
</ul>
<ul>
<li><strong>오디오 서버(Audio Server)</strong>
사용자 공간에서 애플리케이션과 ALSA 사이를 중계하는 소프트웨어 계층이다. Ubuntu 24.04에서는 기본적으로 PipeWire가 사용되며, 과거에는 PulseAudio가 주로 사용되었다. 오디오 서버는 출력 장치 선택, 스트림 관리, 믹싱 등을 담당한다. 서버가 비정상 상태이거나 잘못 설정되면 장치가 인식되어도 소리가 출력되지 않을 수 있다.</li>
</ul>
<ul>
<li><strong>ALSA (Advanced Linux Sound Architecture)</strong>
Linux에서 오디오 장치를 제어하는 기본 프레임워크다. 커널 공간의 드라이버와 사용자 공간의 라이브러리로 구성되며, 실제 사운드 카드와 직접 통신하는 계층이다. PipeWire나 PulseAudio 같은 오디오 서버도 내부적으로는 ALSA를 통해 하드웨어에 접근한다. 따라서 ALSA 단계에서 장치가 제대로 인식되지 않으면, 상위 오디오 서버가 정상 동작하더라도 출력이 발생하지 않는다.</li>
</ul>
</blockquote>
<h2 id="2-간단한-명령어로-상태-확인">2. 간단한 명령어로 상태 확인</h2>
<h3 id="2-1-커널-및-플랫폼-정보-확인">2-1. 커널 및 플랫폼 정보 확인</h3>
<p>먼저 현재 사용 중인 커널과 플랫폼 정보를 확인한다.</p>
<pre><code class="language-bash"># 커널 버전 확인
uname -r 

# cpu 모델 확인
cat /proc/cpuinfo | grep &quot;model name&quot; | head -1 </code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/4e609ff3-8bc1-429d-bc55-34477bbe7a3f/image.png" alt=""></p>
<p>시스템이 어떤 하드웨어 세대와 커널 환경에서 동작하고 있는지를 파악하기 위함이다.</p>
<ul>
<li>커널 버전은 해당 시점에 어떤 드라이버와 패치가 포함되어 있을 가능성이 있는지를 가늠하는 기준이 된다.</li>
<li>CPU 모델은 플랫폼 세대를 확인하기 위한 정보이며, 이는 SOF 기반 오디오 구조 사용 여부를 추정하는 단서가 된다.</li>
</ul>
<p>다만, 이 정보만으로 하드웨어 지원 여부를 단정할 수는 없다. 실제 지원 상태는 오디오 컨트롤러가 어떤 드라이버에 바인딩되어 있는지, SOF 펌웨어와 토폴로지가 정상적으로 로딩되었는지를 추가로 확인해야 한다. 따라서 이 절은 환경 컨텍스트를 확보하는 1차 확인 단계에 해당하며, 구체적인 오디오 스택 상태는 다음 단계에서 점검한다.</p>
<h3 id="2-2-오디오-장치-및-드라이버-바인딩-상태-확인">2-2. 오디오 장치 및 드라이버 바인딩 상태 확인</h3>
<p>다음 단계에서는 실제 오디오 장치가 커널에 의해 어떻게 인식되고 있는지를 확인한다.</p>
<pre><code class="language-bash">lspci -nnk | grep -A3 -i audio</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/4dd949b4-4414-4b94-b4a7-42ffcccb7c19/image.png" alt="">
이 명령을 통해 확인할 수 있는 내용은 다음과 같다.</p>
<ul>
<li>시스템에 등록된 오디오 컨트롤러의 종류</li>
<li>해당 장치에 현재 바인딩되어 있는 커널 드라이버</li>
<li>사용 가능한 커널 모듈 목록</li>
</ul>
<p>여기서 중요한 항목은 <code>Kernel driver in use</code>이다. 이 값이 <code>sof-audio-pci-intel-*</code> 계열이라면 SOF 기반 오디오 경로를 사용하고 있는 것이고, <code>snd_hda_intel</code>만 표시된다면 전통적인 HDA 경로를 사용하는 구성일 가능성이 높다.</p>
<p>이 확인은 다음과 같은 판단에 사용된다.</p>
<ul>
<li>커널이 장치를 정상적으로 인식하고 있는지</li>
<li>드라이버가 바인딩되지 않은 상태는 아닌지</li>
<li>SOF 기반 구조인지 여부</li>
</ul>
<p>오디오 장치가 정상적으로 표시되고, 드라이버도 바인딩되어 있다면 커널 레벨에서의 기본 인식은 이루어진 상태로 볼 수 있다. 그러나 이 단계 역시 스피커 출력이 정상임을 보장하지는 않는다. 이는 단지 장치 인식 및 드라이버 바인딩 상태를 확인하는 절차에 해당한다.</p>
<p>다음 단계에서는 실제 ALSA 계층에서 장치가 어떻게 등록되어 있는지를 확인한다.</p>
<h3 id="2-3-alsa-카드-및-포트-상태-확인">2-3. ALSA 카드 및 포트 상태 확인</h3>
<p>커널에서 오디오 장치가 인식되었는지 확인했다면, 다음으로는 ALSA 계층에서 카드가 어떻게 등록되어 있는지를 확인한다.</p>
<pre><code class="language-bash">cat /proc/asound/cards

# 또는
aplay -l</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/8904428d-e5ed-4a1c-9508-76bbab95aba9/image.png" alt=""></p>
<p>이 단계에서 확인하는 내용은 다음과 같다.</p>
<ul>
<li>시스템에 등록된 ALSA 사운드 카드 목록</li>
<li>카드의 논리적 이름(예: <code>sof-hda-dsp</code>, <code>HDA Intel</code> 등)</li>
<li>사용 가능한 PCM 장치</li>
</ul>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>PCM(Pulse Code Modulation) 장치</strong>
ALSA에서 오디오 데이터가 실제로 입출력되는 논리적 인터페이스다. 애플리케이션이 생성한 디지털 오디오 신호는 PCM 장치를 통해 커널 드라이버로 전달된다. 하나의 사운드 카드에는 여러 개의 PCM 장치가 존재할 수 있으며, 각각은 서로 다른 출력 또는 입력 경로에 대응한다. 예를 들어 스피커 출력, 이어폰 출력, HDMI 출력, 마이크 입력 등이 별도의 PCM 장치로 구성될 수 있다. <code>aplay -l</code> 명령에서 표시되는 <code>device 0</code>, <code>device 3</code> 등의 항목이 각각의 PCM 장치에 해당한다. 사운드 카드가 정상적으로 인식되더라도, 특정 PCM 장치가 비활성화되어 있거나 올바른 경로에 매핑되지 않으면 해당 출력으로는 소리가 발생하지 않을 수 있다.</li>
</ul>
</blockquote>
<p>사운드 카드가 정상적으로 표시된다면, 커널 드라이버와 ALSA 계층까지는 연결된 상태로 볼 수 있다.
즉, 오디오 스택의 하위 계층은 기본적으로 동작 중이라는 의미다. 반대로 카드가 보이지 않거나 <code>Dummy</code> 장치만 표시된다면, 드라이버 로딩 실패나 펌웨어 문제 가능성을 우선적으로 고려해야 한다.</p>
<p>이 단계는 다음을 구분하기 위한 절차다.</p>
<ul>
<li>장치 인식 자체가 실패한 상태인지</li>
<li>장치는 인식되었지만 출력 경로에서 문제가 발생하는지</li>
</ul>
<p>카드가 정상 등록되어 있다면, 문제는 단순 인식 실패가 아니라 출력 경로, 프로파일, 또는 모델별 설정 정의 영역에 있을 가능성이 높아진다. </p>
<p>다음 단계에서는 사용자 공간 오디오 서버와 실제 출력 포트 상태를 확인한다.</p>
<h3 id="2-4-오디오-서버-및-출력-포트-상태-확인">2-4. 오디오 서버 및 출력 포트 상태 확인</h3>
<p>커널과 ALSA 계층까지 정상적으로 인식되었다면, 다음으로는 사용자 공간 오디오 서버와 실제 출력 포트 상태를 확인한다. Ubuntu 24.04에서는 기본적으로 PipeWire가 사용된다. 다음 명령으로 현재 서버 상태를 확인할 수 있다.</p>
<pre><code class="language-bash">pactl info</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/206771b7-b38c-4464-8957-546a568ee3cf/image.png" alt=""></p>
<p>또는 이 명령어로도 현재 Pipewire 상태를 확인할 수 있다.</p>
<pre><code class="language-bash">systemctl --user status pipewire</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/e02e1e8c-356c-447d-b022-b3eb6770fe44/image.png" alt=""></p>
<p>이를 통해 확인할 수 있는 내용은 다음과 같다.</p>
<ul>
<li>현재 사용 중인 오디오 서버가 PipeWire인지 PulseAudio인지</li>
<li>서버 프로세스가 정상적으로 실행 중인지</li>
</ul>
<p>오디오 서버가 정상 동작 중이라면, 애플리케이션과 ALSA 사이의 중계 계층은 살아 있는 상태다.</p>
<p>다음으로 실제 출력 포트 상태를 확인한다.</p>
<pre><code class="language-bash">wpctl status

# 또는
pactl list sinks</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/223f8a93-f326-4b46-85f7-a5a162be058e/image.png" alt=""></p>
<p>이 단계에서 확인하는 항목은 다음과 같다.</p>
<ul>
<li>Speaker 포트가 존재하는지</li>
<li>기본 출력 장치가 무엇으로 설정되어 있는지</li>
<li>Dummy Output으로 전환되어 있지는 않은지</li>
<li>HDMI 출력이 기본값으로 설정되어 있지는 않은지</li>
</ul>
<p>스피커 포트가 존재하고 선택도 가능하다면, 문제는 단순한 “장치 미인식”이 아니라 출력 경로 또는 모델별 정의 문제일 가능성이 높다.</p>
<h3 id="2-5-alsa-mixer-상태-확인">2-5. ALSA Mixer 상태 확인</h3>
<p>커널, 드라이버, ALSA 카드, 오디오 서버까지 정상적으로 인식되는 것이 확인되었다면, 다음으로는 실제 믹서 레벨에서 출력 경로가 활성화되어 있는지를 점검한다.</p>
<pre><code class="language-bash">alsamixer</code></pre>
<p>명령을 실행한 뒤, <code>F6</code> 키를 눌러 올바른 사운드 카드를 선택한다. 이 단계에서 확인할 항목은 다음과 같다.</p>
<ul>
<li><code>Master</code> 및 <code>Speaker</code> 볼륨이 0으로 설정되어 있지 않은지</li>
<li><code>Speaker</code> 또는 관련 항목이 <code>MM</code>(mute) 상태가 아닌지</li>
<li><code>Auto-Mute</code>가 활성화되어 있지 않은지</li>
<li><code>Headphone</code> 포트가 잘못 감지되어 스피커가 차단되고 있지 않은지</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/5a63ccdb-8ba4-42ae-a17a-6d6468271de5/image.png" alt=""></p>
<p>ALSA Mixer는 하드웨어에 가까운 레벨의 믹서 상태를 보여주기 때문에, 단순한 음소거 또는 포트 차단 문제를 빠르게 확인할 수 있다.</p>
<p>이 단계에서 스피커가 음소거 상태이거나 Auto-Mute로 인해 차단되어 있다면, 이는 가설 B(포트 전환/설정 문제)에 해당한다. 설정을 수정한 뒤 즉시 출력이 발생하는지 확인하면 된다.</p>
<p>반대로, 믹서 항목이 정상적으로 활성화되어 있고 볼륨도 충분히 설정되어 있음에도 출력이 발생하지 않는다면, 문제는 단순한 포트 선택이나 음소거 상태가 아닐 가능성이 높다. 이 경우 가설 B는 상당 부분 배제되며, 앰프 드라이버 또는 모델별 경로 정의 문제로 범위를 좁힐 수 있다.</p>
<p>이 단계까지 확인했음에도 스피커가 여전히 무음이라면, 다음으로는 스피커 앰프 존재 여부와 관련 드라이버 로딩 상태를 점검한다.</p>
<h2 id="3-스피커-경로의-하드웨어-지원-확인">3. 스피커 경로의 하드웨어 지원 확인</h2>
<p>2단계까지의 점검을 통해 다음 사항이 확인되었다.</p>
<ul>
<li>커널과 드라이버는 정상적으로 바인딩되어 있음</li>
<li>ALSA 카드와 PCM 장치는 등록되어 있음</li>
<li>오디오 서버도 정상 동작 중</li>
<li>ALSA Mixer에서 음소거 또는 포트 차단 문제는 없음</li>
</ul>
<p>그럼에도 불구하고 스피커 출력이 발생하지 않는다면, 문제는 단순 설정이 아니라 스피커 하드웨어 경로에 있을 가능성이 높다. 이를 확인하기 위해 스피커가 실제로 어떤 구조로 구동되는지 단계적으로 점검하였다.</p>
<h3 id="3-1-코덱-이후-경로-구조-확인">3-1. 코덱 이후 경로 구조 확인</h3>
<p>먼저, 시스템에서 사용 중인 오디오 코덱을 확인한다.</p>
<pre><code class="language-bash">cat /proc/asound/card0/codec* | grep Codec</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/c1df47dd-c28b-40b2-b79f-7fc49bc58554/image.png" alt=""></p>
<p>출력 결과에서 Realtek 계열 코덱이 확인된다면, 이어폰 출력은 해당 코덱을 통해 직접 구동될 가능성이 높다. 이 시점에서 확인되는 사실은 다음과 같다.</p>
<ul>
<li>이어폰은 정상 출력</li>
<li>코덱은 정상 인식</li>
<li>ALSA Mixer 설정도 정상</li>
</ul>
<p>따라서 코덱 이전 단계(App → Audio Server → ALSA → Driver → Codec)는 정상 동작 중으로 판단할 수 있다.</p>
<p>그러나 최근 노트북 설계에서는 스피커가 코덱에 직접 연결되지 않고, 별도의 스마트 앰프를 통해 구동되는 경우가 많다. 이 경우 스피커 경로는 “코덱 이후 단계”에서 추가적인 제어가 필요하다. 즉, 이어폰이 정상이라고 해서 스피커 경로도 동일하다고 단정할 수는 없다.</p>
<h3 id="3-2-스마트-앰프-존재-여부-탐색-절차">3-2. 스마트 앰프 존재 여부 탐색 절차</h3>
<p>스피커가 무음이고, 코덱 및 ALSA 설정이 정상이라면 스피커가 별도의 스마트 앰프를 통해 구동되는 구조일 가능성을 고려해야 한다. 다만, 어떤 제조사의 앰프가 사용되었는지 사전에 알 수 없는 경우가 많다.</p>
<p>이 경우에는 특정 모델명을 기준으로 검색하기보다, 시스템 버스와 커널 로그를 기준으로 탐색하는 것이 합리적이다.</p>
<blockquote>
<p><strong>용어 설명</strong></p>
<ul>
<li><strong>커널 로그(Kernel Log)</strong>
커널이 부팅 과정과 장치 초기화 과정에서 출력하는 메시지 기록이다. <code>dmesg</code> 명령을 통해 확인할 수 있으며, 하드웨어 인식, 드라이버 로딩, 초기화 실패 등의 정보가 포함된다. 오디오 문제 분석에서는 장치가 실제로 프로브되었는지, 초기화에 실패했는지를 확인하는 중요한 단서가 된다.</li>
</ul>
<ul>
<li><strong>Probe(프로브)</strong>
커널이 특정 하드웨어 장치를 감지하고 해당 드라이버와 연결하려는 초기화 과정이다. 장치가 발견되면 드라이버가 이를 “프로브”하며, 성공 여부가 커널 로그에 기록된다. 프로브가 실패하면 해당 장치는 인식되었더라도 정상 동작하지 않을 수 있다.</li>
</ul>
<ul>
<li><strong>I2C(Inter-Integrated Circuit)</strong>
저속 직렬 통신 버스로, 노트북 내부에서 센서나 앰프 같은 소형 장치를 연결하는 데 사용된다. 많은 스마트 앰프는 I2C 버스를 통해 CPU와 통신하며, <code>/sys/bus/i2c/devices/</code> 경로에서 해당 장치를 확인할 수 있다.</li>
</ul>
<ul>
<li><strong>SoundWire</strong>
Intel 최신 플랫폼에서 오디오 컴포넌트를 연결하기 위해 사용하는 디지털 오디오 버스다. 코덱이나 앰프가 SoundWire를 통해 연결되는 경우, <code>/sys/bus/soundwire/</code> 경로와 관련 커널 로그에서 해당 장치를 확인할 수 있다.</li>
</ul>
<ul>
<li><strong>시스템 버스(System Bus)</strong>
CPU와 주변 장치 간의 통신 경로를 의미한다. PCIe, I2C, SoundWire 등이 이에 해당한다. 오디오 문제를 분석할 때는 단순히 “드라이버가 있는가”뿐 아니라, 해당 장치가 어떤 버스를 통해 연결되어 있는지를 확인하는 것이 중요하다.</li>
</ul>
</blockquote>
<p>먼저 오디오 초기화 로그를 폭넓게 확인한다.</p>
<pre><code class="language-bash">dmesg | grep -iE &#39;sof|snd|asoc|i2c|soundwire|probe|fail&#39;</code></pre>
<p>이 명령은 특정 제조사명을 가정하지 않고, 오디오 서브시스템이 부팅 중 어떤 장치를 초기화했는지를 확인하기 위한 것이다. 여기서 다음과 같은 패턴이 나타날 수 있다.</p>
<ul>
<li><code>snd_soc_*</code></li>
<li><code>ASoC:</code></li>
<li><code>i2c 1-00xx</code></li>
<li><code>probe</code></li>
<li><code>failed</code></li>
<li><code>component</code></li>
</ul>
<p>이 메시지들은 코덱 외에 추가적인 오디오 컴포넌트가 존재함을 시사한다. 특히 I2C 주소 형태(<code>1-0038</code> 등)가 보인다면, 해당 주소에 별도의 장치가 연결되어 있을 가능성이 높다.</p>
<p>이 단서를 바탕으로 I2C 장치를 확인한다.</p>
<pre><code class="language-bash">ls /sys/bus/i2c/devices/

# 또는
dmesg | grep -i i2c</code></pre>
<p>이 단계에서 확인할 수 있는 것은 다음과 같다.</p>
<ul>
<li>I2C 장치로 등록된 오디오 관련 디바이스 존재 여부</li>
<li>장치명에 포함된 제조사 식별자</li>
</ul>
<p>이 과정에서 <code>max98390</code>이라는 문자열이 로그에 등장한다면, 이는 해당 장치가 스피커 경로에 관여하는 스마트 앰프일 가능성을 강하게 시사한다.</p>
<p>Intel 최신 플랫폼의 경우 SoundWire 기반 구조도 고려할 수 있다.</p>
<pre><code class="language-bash">ls /sys/bus/soundwire/devices/

# 또는
dmesg | grep -i soundwire</code></pre>
<p>SoundWire 장치가 존재한다면 해당 경로의 오디오 컴포넌트를 추가로 확인해야 한다.</p>
<p>다음으로, 로드된 커널 모듈을 점검한다.</p>
<pre><code class="language-bash">lsmod | grep -E &#39;snd|soc|max|cs|tas|rt&#39;</code></pre>
<p>여기서 일반적인 <code>snd_hda_intel</code> 외에 SoC 계열 모듈이나 특정 앰프 관련 모듈이 존재하는지를 확인한다.</p>
<p>마지막으로, 보다 넓은 범위에서 초기화 로그를 다시 점검한다.</p>
<pre><code class="language-bash">dmesg | grep -iE &#39;audio|amp|speaker|firmware&#39;</code></pre>
<p>이 로그를 통해 다음을 확인할 수 있다.</p>
<ul>
<li>특정 앰프 장치에 대한 프로브 시도</li>
<li>펌웨어 로딩 실패 여부</li>
<li>장치 등록 오류</li>
</ul>
<p>이 과정에서 <code>max98390</code>이라는 장치명이 반복적으로 나타난다면, 이는 스피커 경로에 해당 앰프가 존재함을 의미한다. 이어폰은 정상적으로 동작하지만 스피커만 무음인 상황과 결합하면, 스피커 출력이 이 앰프를 통해 이루어지고 있으며 해당 드라이버 또는 초기화 경로가 완성되지 않았을 가능성을 합리적으로 추론할 수 있다.</p>
<p>이 시점에서 <code>max98390</code>은 단순한 로그 문자열이 아니라, 스피커 경로를 구성하는 핵심 컴포넌트로 인식되기 시작한다. 문제는 더 이상 포트 전환이나 믹서 설정이 아니라, 해당 스마트 앰프의 드라이버 지원 여부로 좁혀진다.</p>
<h2 id="4-max98390-hda-앰프-드라이버-지원-여부-확인-및-적용">4. MAX98390 HDA 앰프 드라이버 지원 여부 확인 및 적용</h2>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/491f479a-57bb-45cc-8a10-6325319d2c5f/image.png" alt=""></p>
<p>로그 분석 과정에서 <code>max98390</code>이라는 스마트 앰프 장치명이 확인되었고, 이어폰은 정상이나 스피커만 무음인 증상과 일치했다. 이에 따라 해당 앰프에 대한 드라이버 지원 여부를 확인하였다.</p>
<p>모델명과 <code>max98390</code>을 조합해 검색한 결과, Galaxy Book4/5 계열에서 기본 스피커가 동작하지 않는 사례가 보고되어 있었고, 공통적으로 다음과 같은 설명이 확인되었다.</p>
<blockquote>
<p>missing MAX98390 HDA driver</p>
</blockquote>
<p>이는 현재 사용 중인 Ubuntu 커널에 MAX98390 HDA 앰프용 드라이버가 포함되어 있지 않음을 의미한다.</p>
<h3 id="4-1-왜-기본-커널에서-동작하지-않았는가">4-1. 왜 기본 커널에서 동작하지 않았는가</h3>
<p>Galaxy Book4/5는 Intel 최신 플랫폼과 Realtek 코덱, 그리고 MAX98390 스마트 앰프 조합을 사용한다. 그러나 해당 MAX98390 HDA 앰프 드라이버는 메인라인 커널에 아직 완전히 통합되지 않았거나, 사용 중인 커널 버전에 포함되어 있지 않은 상태였다.</p>
<p>그 결과 시스템은 다음과 같은 상태가 된다.</p>
<ul>
<li>사운드 카드 인식 정상</li>
<li>SOF 기반 오디오 경로 정상</li>
<li>코덱 인식 정상</li>
<li>이어폰 출력 정상</li>
<li>그러나 스피커 앰프 제어 드라이버 부재</li>
</ul>
<p>즉, 스피커 경로의 마지막 단계가 비어 있는 구조다.</p>
<h3 id="4-2-dkmsout-of-tree-모듈-방식의-의미">4-2. DKMS(out-of-tree) 모듈 방식의 의미</h3>
<p>해당 문제를 해결하기 위해 사용된 방법은 DKMS 기반 out-of-tree 커널 모듈이다.</p>
<ul>
<li><strong>DKMS(Dynamic Kernel Module Support)</strong>는 커널 외부에 있는 모듈을 설치하고, 커널 업데이트 시 자동으로 재빌드되도록 관리하는 시스템이다.</li>
<li><strong>out-of-tree</strong> 모듈은 메인라인 커널에 포함되지 않은 별도의 드라이버 코드를 의미한다.</li>
</ul>
<p><a href="https://github.com/Andycodeman/samsung-galaxy-book4-linux-fixes">https://github.com/Andycodeman/samsung-galaxy-book4-linux-fixes</a></p>
<p>이 레포는 메인라인에 아직 통합되지 않은 MAX98390 HDA 앰프 드라이버를 DKMS 방식으로 제공한다. 즉, 기본 커널의 공백을 임시로 메우는 방식이다.</p>
<h3 id="4-3-적용-절차">4-3. 적용 절차</h3>
<pre><code class="language-bash">curl -sL https://github.com/Andycodeman/samsung-galaxy-book4-linux-fixes/archive/refs/heads/main.tar.gz | tar xz &amp;&amp; cd samsung-galaxy-book4-linux-fixes-main/speaker-fix &amp;&amp; sudo ./install.sh &amp;&amp; sudo reboot</code></pre>
<p>설치 과정에서 DKMS가 모듈을 빌드하고 현재 커널에 등록한다. 설치가 완료되면 다음을 확인한다.</p>
<pre><code class="language-bash">lsmod | grep max

#또는
dmesg | grep -i max</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_hammy/post/b501531f-2a66-4311-9b0f-9c65f8e22e4f/image.png" alt=""></p>
<p>이제 <code>max98390</code> 관련 드라이버가 정상적으로 로딩되었는지 확인할 수 있다. 재부팅 후 스피커 출력이 정상 동작한다면, 원인은 앰프 드라이버 부재였음이 확정된다.</p>
<h3 id="4-4-이-단계에서의-의미">4-4. 이 단계에서의 의미</h3>
<p>이 시점에서 전체 원인은 다음과 같이 정리된다.</p>
<ul>
<li>가설 B(포트/설정 문제) 배제</li>
<li>가설 D(드라이버 바인딩 실패) 배제</li>
<li>가설 A/C 영역 중 “앰프 드라이버 부재”로 확정</li>
</ul>
<p>문제는 단순한 설정 오류가 아니라, 해당 모델에 필요한 스마트 앰프 드라이버가 기본 커널에 포함되지 않았던 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[pandas, numpy 자주 사용하는 메서드]]></title>
            <link>https://velog.io/@dev_hammy/pandas-numpy-%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%A9%94%EC%84%9C%EB%93%9C</link>
            <guid>https://velog.io/@dev_hammy/pandas-numpy-%EC%9E%90%EC%A3%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%A9%94%EC%84%9C%EB%93%9C</guid>
            <pubDate>Thu, 07 Nov 2024 04:58:53 GMT</pubDate>
            <description><![CDATA[<p>아래는 <strong><code>pandas</code>와 <code>numpy</code>에서 자주 사용되는 메서드들</strong>을 입력 자료형(input type)과 출력 자료형(output type)을 명시하여 표로 정리한 것입니다.</p>
<table>
<thead>
<tr>
<th>Library</th>
<th>Method</th>
<th>Input Type</th>
<th>Output Type</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>pandas</td>
<td><code>DataFrame.loc[]</code></td>
<td><code>DataFrame</code> + <code>label</code> or <code>slice</code></td>
<td><code>DataFrame</code> or <code>Series</code></td>
<td>Row/column selection by label.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.iloc[]</code></td>
<td><code>DataFrame</code> + <code>int</code> or <code>slice</code></td>
<td><code>DataFrame</code> or <code>Series</code></td>
<td>Row/column selection by integer position.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.groupby()</code></td>
<td><code>DataFrame</code> + <code>label</code> or <code>list</code></td>
<td><code>DataFrameGroupBy</code></td>
<td>Groups data by one or more columns.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.mean()</code></td>
<td><code>DataFrame</code></td>
<td><code>Series</code> or <code>float</code></td>
<td>Computes mean for each column (or total mean if axis specified).</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.sum()</code></td>
<td><code>DataFrame</code></td>
<td><code>Series</code> or <code>float</code></td>
<td>Computes sum for each column (or total sum if axis specified).</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.describe()</code></td>
<td><code>DataFrame</code></td>
<td><code>DataFrame</code></td>
<td>Generates descriptive statistics for numeric columns.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.drop()</code></td>
<td><code>DataFrame</code>, <code>label</code> or <code>list</code></td>
<td><code>DataFrame</code></td>
<td>Drops specified rows/columns.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.apply()</code></td>
<td><code>DataFrame</code>, <code>function</code></td>
<td>Varies (often <code>DataFrame</code> or <code>Series</code>)</td>
<td>Applies function to each element, column, or row.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>Series.to_frame()</code></td>
<td><code>Series</code></td>
<td><code>DataFrame</code></td>
<td>Converts series to DataFrame with single column.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.T</code></td>
<td><code>DataFrame</code></td>
<td><code>DataFrame</code></td>
<td>Transposes the DataFrame, switching rows and columns.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>Series.unique()</code></td>
<td><code>Series</code></td>
<td><code>ndarray</code></td>
<td>Returns unique values in a Series.</td>
</tr>
<tr>
<td>pandas</td>
<td><code>DataFrame.fillna()</code></td>
<td><code>DataFrame</code>, <code>value</code> or <code>method</code></td>
<td><code>DataFrame</code></td>
<td>Fills missing values.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.mean()</code></td>
<td><code>ndarray</code>, <code>list</code>, <code>tuple</code></td>
<td><code>float</code> or <code>ndarray</code></td>
<td>Calculates mean of array elements.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.sum()</code></td>
<td><code>ndarray</code>, <code>list</code>, <code>tuple</code></td>
<td><code>float</code>, <code>int</code>, or <code>ndarray</code></td>
<td>Sums array elements along specified axis.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.reshape()</code></td>
<td><code>ndarray</code> + <code>tuple</code> (new shape)</td>
<td><code>ndarray</code></td>
<td>Reshapes array to specified dimensions.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.arange()</code></td>
<td><code>int</code> or <code>float</code> start, stop, step</td>
<td><code>ndarray</code></td>
<td>Returns array with evenly spaced values.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.linspace()</code></td>
<td><code>float</code> start, stop, num</td>
<td><code>ndarray</code></td>
<td>Returns array with linearly spaced values between two numbers.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.random.rand()</code></td>
<td><code>int</code> or <code>tuple</code> shape</td>
<td><code>ndarray</code></td>
<td>Generates random values in a given shape.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.argmax()</code></td>
<td><code>ndarray</code>, <code>axis</code></td>
<td><code>int</code> or <code>ndarray</code></td>
<td>Returns indices of maximum values along an axis.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.concatenate()</code></td>
<td><code>tuple</code> or <code>list</code> of arrays</td>
<td><code>ndarray</code></td>
<td>Joins multiple arrays along an axis.</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.hstack()</code></td>
<td><code>tuple</code> or <code>list</code> of arrays</td>
<td><code>ndarray</code></td>
<td>Stacks arrays horizontally (column-wise).</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.vstack()</code></td>
<td><code>tuple</code> or <code>list</code> of arrays</td>
<td><code>ndarray</code></td>
<td>Stacks arrays vertically (row-wise).</td>
</tr>
<tr>
<td>numpy</td>
<td><code>np.where()</code></td>
<td><code>condition</code>, <code>x</code>, <code>y</code></td>
<td><code>ndarray</code></td>
<td>Returns elements based on condition.</td>
</tr>
</tbody></table>
<p>이 표는 각 라이브러리에서 자주 쓰이는 메서드의 입력과 출력 자료형을 간단히 정리한 것이며, 실전에서 사용 시에 필요한 파라미터가 더 있을 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Hyperledger Fabric in Blockchain]]></title>
            <link>https://velog.io/@dev_hammy/Hyperledger-Fabric-in-Blockchain</link>
            <guid>https://velog.io/@dev_hammy/Hyperledger-Fabric-in-Blockchain</guid>
            <pubDate>Mon, 15 Jul 2024 13:29:51 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.geeksforgeeks.org/hyperledger-fabric-in-blockchain/">https://www.geeksforgeeks.org/hyperledger-fabric-in-blockchain/</a></p>
<p>하이퍼레저 패브릭(Hyperledger Fabric)은 모듈형 아키텍처를 통해 높은 수준의 기밀성, 유연성, 회복력 및 확장성을 제공하는 분산 원장 솔루션을 구축하기 위한 오픈 소스 플랫폼입니다. 패브릭을 사용하여 개발된 솔루션은 모든 산업에 맞게 조정될 수 있습니다. 이는 리눅스 재단에서 관리하는 비공개 및 기밀 블록체인 프레임워크입니다. 이 글에서는 하이퍼레저 패브릭에 대해 다음 주제를 다룰 것입니다:</p>
<ol>
<li>하이퍼레저 패브릭이란 무엇인가?</li>
<li>하이퍼레저 패브릭은 어떻게 작동하는가?</li>
<li>하이퍼레저 패브릭의 합의 알고리즘</li>
<li>하이퍼레저 패브릭의 산업별 사용 사례</li>
<li>하이퍼레저 패브릭의 장점</li>
<li>하이퍼레저 패브릭의 한계</li>
</ol>
<h3 id="하이퍼레저-패브릭이란-무엇인가">하이퍼레저 패브릭이란 무엇인가?</h3>
<p>하이퍼레저 패브릭은 엔터프라이즈 수준의 애플리케이션을 위해 설계되었으며, 모듈형 아키텍처, 허가된 네트워크 및 스마트 계약 기능(“체인코드”라고도 함)을 특징으로 합니다.</p>
<p>이 플랫폼은 높은 수준의 보안, 프라이버시 및 확장성을 제공하며, 금융, 공급망 및 헬스케어와 같은 다양한 산업의 다양한 사용 사례에 맞춘 맞춤형 블록체인 솔루션 개발을 지원합니다.
하이퍼레저 패브릭은 각 노드가 거래를 검증하고, 원장을 유지하며, 체인코드를 실행하는 등 특정 기능을 수행하는 노드 네트워크로 작동합니다.
거래는 합의 메커니즘에 의해 검증되고 순서가 지정되어 원장의 무결성과 일관성을 보장합니다.</p>
<h3 id="하이퍼레저-패브릭은-어떻게-작동하는가">하이퍼레저 패브릭은 어떻게 작동하는가?</h3>
<h4 id="구성-요소">구성 요소:</h4>
<p>하이퍼레저 패브릭은 엔터프라이즈 수준의 허가된 블록체인 네트워크입니다. 이는 특정 목적을 위해 상호 작용하는 다양한 고유 조직 또는 구성원으로 구성됩니다. 예를 들어, 이러한 조직은 은행, 금융 기관 또는 공급망 네트워크일 수 있습니다. 각 조직은 식별되며 패브릭 인증 기관을 갖추고 있습니다. 이러한 조직을 멤버라고 합니다.
패브릭의 각 멤버는 패브릭 인증 기관을 사용하여 네트워크에 참여하기 위해 하나 이상의 승인된 피어를 설정할 수 있습니다. 이 모든 피어는 적절히 승인되어야 합니다.
특정 프로그래밍 언어의 소프트웨어 개발 키트(SDK)로 작성된 클라이언트 측 애플리케이션이 네트워크에 연결됩니다.</p>
<h4 id="워크플로우">워크플로우:</h4>
<p>각 거래마다 다음 단계를 따릅니다-</p>
<ol>
<li><strong>제안서 작성:</strong> 스마트폰 제조 회사와 스마트폰 딜러 간의 거래를 상상해보십시오. 거래는 클라이언트 애플리케이션 또는 포털의 도움으로 멤버 조직이 거래 요청을 제안하거나 호출할 때 시작됩니다. 그런 다음 클라이언트 애플리케이션은 제안서를 각 조직의 피어에게 보냅니다.</li>
<li><strong>거래 승인:</strong> 제안서가 승인 피어(제안서 승인을 위한 각 조직의 피어)에게 도달하면 피어는 요청 멤버의 패브릭 인증 기관과 거래를 인증하는 데 필요한 기타 세부 정보를 확인합니다. 그런 다음 체인 코드를 실행하고 응답을 반환합니다. 이 응답은 해당 거래의 승인 또는 거부를 나타냅니다. 응답은 클라이언트에게 전달됩니다.</li>
<li><strong>주문 서비스 제출:</strong> 승인 결과를 받은 후 승인된 거래는 클라이언트 측 애플리케이션에 의해 주문 서비스에 전송됩니다. 주문 서비스를 담당하는 피어는 거래를 특정 블록에 포함시키고 이를 네트워크의 다른 멤버 피어 노드에 보냅니다.</li>
<li><strong>원장 업데이트:</strong> 이 블록을 받은 후 각 조직의 피어 노드는 이 블록으로 로컬 원장을 업데이트합니다. 따라서 새로운 거래가 이제 커밋됩니다.</li>
</ol>
<p><img src="https://media.geeksforgeeks.org/wp-content/uploads/20210726200928/dgw26071.png" alt=""></p>
<h3 id="하이퍼레저-패브릭의-합의-알고리즘">하이퍼레저 패브릭의 합의 알고리즘</h3>
<p>하이퍼레저 패브릭은 네트워크 참가자들이 공유 원장의 내용에 대해 동의할 수 있도록 합의 알고리즘을 사용합니다. 하이퍼레저 패브릭의 합의 알고리즘은 플러그형으로, 필요에 따라 다른 알고리즘으로 교체할 수 있습니다.</p>
<p>하이퍼레저 패브릭에서 가장 일반적으로 사용되는 합의 알고리즘은 다음과 같습니다:</p>
<ul>
<li><strong>실용 비잔틴 장애 허용(PBFT)</strong>: PBFT는 네트워크에서 장애 허용성과 신뢰성을 제공하는 합의 알고리즘입니다. 신뢰할 수 있고 잘 알려진 참가자가 제한된 네트워크에 적합합니다.</li>
<li><strong>RAFT</strong>: RAFT는 여러 노드에 걸쳐 일관된 상태를 유지하는 데 사용되는 합의 알고리즘입니다. 참가자가 알려지지 않았거나 잠재적으로 신뢰할 수 없는 네트워크에 적합합니다.</li>
<li><strong>Solo</strong>: Solo는 단일 노드 네트워크에서 테스트 목적으로 사용되는 합의 알고리즘입니다. 실제 운영에는 적합하지 않습니다.</li>
</ul>
<h3 id="하이퍼레저-패브릭의-산업별-사용-사례">하이퍼레저 패브릭의 산업별 사용 사례</h3>
<ol>
<li><p><strong>공급망</strong>: 공급망은 특정 제품의 공급자, 제조업체 및 소매업체로 구성된 글로벌 또는 지역 네트워크입니다. 하이퍼레저 패브릭 네트워크는 거래의 투명성과 추적 가능성을 높여 공급망의 거래 프로세스를 개선할 수 있습니다. 패브릭 네트워크에서는 원장에 접근할 수 있는 인증된 기업이 이전 거래 데이터를 볼 수 있습니다. 이는 책임성을 높이고 거래의 위조 위험을 줄입니다. 실시간 생산 및 배송 업데이트가 원장에 기록될 수 있어 제품 상태를 더 빠르고 간단하며 효율적으로 추적할 수 있습니다.</p>
</li>
<li><p><strong>거래 및 자산 이전</strong>: 거래 및 자산 이전은 수입업자, 수출업자, 은행, 중개인 등 여러 조직이나 회원이 필요합니다. 이들은 상호 작용하며, 디지털 시대에도 많은 서류 작업이 진행됩니다. 그러나 하이퍼레저를 사용하면 종이 없는 방식으로 거래하고 상호 작용할 수 있습니다. 하이퍼레저 패브릭은 신뢰할 수 있는 기관이 서명한 문서와 동일한 신뢰 계층을 추가할 수 있습니다. 이는 시스템의 성능을 향상시키기도 합니다. 또한, 하이퍼레저 패브릭을 통해 자산을 블록체인 네트워크에서 비물질화할 수 있습니다. 이를 통해 거래자나 이해관계자는 금융 증권에 직접 접근할 수 있으며 언제든지 거래할 수 있습니다.</p>
</li>
<li><p><strong>보험</strong>: 보험 산업은 사기 또는 허위 청구를 방지하기 위해 수십억 달러를 지출합니다. 하이퍼레저 패브릭을 사용하면 보험 회사는 원장에 저장된 거래 데이터를 참조할 수 있습니다. 하이퍼레저 패브릭은 체인 코드를 사용하여 청구 처리를 자동화하고 지불을 신속하게 할 수 있습니다. 이는 다자간 구상 청구 처리에도 유용합니다. 여기서 과실 당사자로부터 보험 회사로의 상환을 자동화할 수 있습니다. 신원 확인 또는 KYC 프로세스도 이 비공개 블록체인을 사용하면 용이해집니다.</p>
</li>
</ol>
<h3 id="하이퍼레저-패브릭의-장점">하이퍼레저 패브릭의 장점</h3>
<ol>
<li><p><strong>오픈 소스</strong>: 하이퍼레저 패브릭은 리눅스 재단이 주관하는 오픈 소스 블록체인 프레임워크입니다. 활발한 개발자 커뮤니티가 있으며, 코드는 공개적으로 접근할 수 있도록 설계되었습니다. 누구나 코드를 보고 수정하고 배포할 수 있습니다. 전 세계의 사람들이 소스 코드 개발에 도움을 줄 수 있습니다.</p>
</li>
<li><p><strong>비공개 및 기밀성</strong>: 공공 블록체인 네트워크에서는 네트워크의 모든 노드가 전체 원장의 사본을 받습니다. 따라서 모든 것이 모두에게 공개되므로 프라이버시가 큰 문제가 됩니다. 또한, 모든 참가자의 신원이 알려지지 않고 인증되지 않습니다. 누구나 공공 블록체인에 참여할 수 있습니다. 그러나 하이퍼레저 패브릭에서는 모든 참가자의 신원이 인증됩니다. 그리고 원장은 인증된 멤버에게만 공개됩니다. 이 점은 은행, 보험 등 고객 데이터를 기밀로 유지해야 하는 산업에서 특히 유용합니다.</p>
</li>
<li><p><strong>액세스 제어</strong>: 하이퍼레저 패브릭에서는 물리적 블록체인 네트워크 위에 가상 블록체인 네트워크가 있습니다. 이 네트워크는 자체 액세스 규칙을 가지고 있습니다. 이는 자체 거래 순서 지정 메커니즘을 사용하며 추가적인 액세스 제어 계층을 제공합니다. 이는 멤버가 데이터 노출을 제한하고 기밀로 유지하고자 할 때 특히 유용합니다. 예를 들어 두 경쟁자가 같은 네트워크에 있을 때, 패브릭은 비공개 데이터 수집 및 접근성을 제공하여 한 경쟁자가 자신의 데이터를 다른 경쟁자에게 노출되지 않도록 제어할 수 있습니다.</p>
</li>
<li><p><strong>체인코드 기능</strong>: 체인코드라는 스마트 계약을 호스팅하기 위해 컨테이너 기술을 포함하며, 이는 시스템의 비즈니스 규칙을 정의합니다. 이는 다양한 플러그형 구성 요소를 지원하도록 설계되었으며, 경제 전반에 걸친 복잡성을 수용할 수 있습니다. 이는 자산 소유권 변경과 같은 특정 거래 유형에 유용합니다.</p>
</li>
<li><p><strong>성능</strong>: 하이퍼레저 패브릭은 비공개 블록체인 네트워크이므로 이 네트워크에서 거래를 검증할 필요가 없어 거래 속도가 빨라지고 성능이 향상됩니다.</p>
</li>
</ol>
<h3 id="하이퍼레저-패브릭의-한계">하이퍼레저 패브릭의 한계</h3>
<p>하이퍼레저 패브릭은 블록체인 애플리케이션을 개발하기 위한 강력하고 유연한 플랫폼이지만, 다른 기술과 마찬가지로 몇 가지 한계가 있습니다:</p>
<ol>
<li><strong>확장성</strong>: 하이퍼레저 패브릭은 참가자가 알려지고 신뢰할 수 있는 허가된 네트워크를 위해 설계되었으므로 대규모 공공 네트워크에서는 확장성이 제한될 수 있습니다.</li>
<li><strong>성능</strong>: 하이퍼레저 패브릭의 성능은 네트워크 크기, 네트워크 구성 및 체인코드의 복잡성과 같은 요인에 영향을 받을 수 있으며, 이는 대량 거래를 처리하는 능력을 제한할 수 있습니다.</li>
<li><strong>복잡성</strong>: 하이퍼레저 패브릭 네트워크를 설정하고 구성하는 것은 복잡할 수 있으며, 기술 및 구성 요소에 대한 깊은 이해가 필요합니다.</li>
<li><strong>호환성</strong>: 하이퍼레저 패브릭은 Go와 JavaScript와 같은 특정 프로그래밍 언어와 함께 사용되도록 설계되었으므로 다른 기술 및 프로그래밍 언어와의 호환성이 제한될 수 있습니다.</li>
<li><strong>비용</strong>: 하이퍼레저 패브릭 네트워크를 운영하려면 인프라 및 자원이 필요하므로 블록체인 애플리케이션의 배포 및 운영에 비용이 추가될 수 있습니다.</li>
<li><strong>상호 운용성</strong>: 하이퍼레저 패브릭은 단일 네트워크 내에서 사용하도록 설계되었으며, 다른 블록체인 플랫폼과의 상호 운용성은 제한적입니다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Locking]]></title>
            <link>https://velog.io/@dev_hammy/Locking</link>
            <guid>https://velog.io/@dev_hammy/Locking</guid>
            <pubDate>Sun, 21 Apr 2024 08:58:32 GMT</pubDate>
            <description><![CDATA[<p>사용할 잠금 모드를 지정하려면 다음 예제와 같이 쿼리 메서드에 <code>@Lock</code> annotation을 사용할 수 있습니다.</p>
<p>Example 1. Defining lock metadata on query methods</p>
<pre><code class="language-java">interface UserRepository extends Repository&lt;User, Long&gt; {

  // Plain query method
  @Lock(LockModeType.READ)
  List&lt;User&gt; findByLastname(String lastname);
}</code></pre>
<p>이 메소드 선언으로 인해 트리거되는 쿼리에 <code>LockModeType</code>이 <code>READ</code>로 제공됩니다. 다음 예와 같이 저장소 인터페이스에서 CRUD 메서드를 다시 선언하고 <code>@Lock</code> annotation을 추가하여 해당 메서드에 대한 잠금을 정의할 수도 있습니다.</p>
<p>Example 2. Defining lock metadata on CRUD methods</p>
<pre><code class="language-java">interface UserRepository extends Repository&lt;User, Long&gt; {

  // Redeclaration of a CRUD method
  @Lock(LockModeType.READ)
  List&lt;User&gt; findAll();
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Transactionality]]></title>
            <link>https://velog.io/@dev_hammy/Transactionality</link>
            <guid>https://velog.io/@dev_hammy/Transactionality</guid>
            <pubDate>Sun, 21 Apr 2024 08:56:50 GMT</pubDate>
            <description><![CDATA[<p>기본적으로 <code>CrudRepository</code>에서 상속된 메서드는 <code>SimpleJpaRepository</code>에서 트랜잭션 구성을 상속합니다. 읽기 작업의 경우 트랜잭션 구성 <code>readOnly</code> 플래그가 <code>true</code>로 설정됩니다. 다른 모든 항목은 기본 트랜잭션 구성이 적용되도록 일반 <code>@Transactional</code>로 구성됩니다. 트랜잭션 저장소 조각이 지원하는 저장소 메서드는 실제 조각 메서드에서 트랜잭션 속성을 상속합니다.</p>
<p>저장소에 선언된 메서드 중 하나에 대한 트랜잭션 구성을 조정해야 하는 경우 다음과 같이 저장소 인터페이스에서 해당 메서드를 다시 선언하세요.</p>
<p>Example 1. Custom transaction configuration for CRUD</p>
<pre><code class="language-java">public interface UserRepository extends CrudRepository&lt;User, Long&gt; {

  @Override
  @Transactional(timeout = 10)
  public List&lt;User&gt; findAll();

  // Further query method declarations
}</code></pre>
<p>그렇게 하면 <code>findAll()</code> 메서드가 <code>readOnly</code> 플래그 없이 10초의 제한 시간으로 실행됩니다.</p>
<p>트랜잭션 동작을 변경하는 또 다른 방법은 (일반적으로) 둘 이상의 저장소를 포함하는 Facade 또는 서비스 구현을 사용하는 것입니다. 그 목적은 CRUD가 아닌 작업에 대한 트랜잭션 경계를 정의하는 것입니다. 다음 예는 둘 이상의 저장소에 대해 이러한 Facade를 사용하는 방법을 보여줍니다.</p>
<p>Example 2. Using a facade to define transactions for multiple repository calls</p>
<pre><code class="language-java">@Service
public class UserManagementImpl implements UserManagement {

  private final UserRepository userRepository;
  private final RoleRepository roleRepository;

  public UserManagementImpl(UserRepository userRepository,
    RoleRepository roleRepository) {
    this.userRepository = userRepository;
    this.roleRepository = roleRepository;
  }

  @Transactional
  public void addRoleToAllUsers(String roleName) {

    Role role = roleRepository.findByName(roleName);

    for (User user : userRepository.findAll()) {
      user.addRole(role);
      userRepository.save(user);
    }
  }
}</code></pre>
<p>이 예에서는 <code>addRoleToAllUsers(…)</code>에 대한 호출이 트랜잭션 내에서 실행되도록 합니다(기존 트랜잭션에 참여하거나 이미 실행 중인 트랜잭션이 없는 경우 새 트랜잭션 생성). 외부 트랜잭션 구성이 실제 사용되는 트랜잭션 구성을 결정하므로 저장소의 트랜잭션 구성은 무시됩니다. 작동할 Facade의 주석 기반 구성을 얻으려면 <code>&lt;tx:annotation-driven /&gt;</code>를 활성화하거나 <code>@EnableTransactionManagement</code>를 명시적으로 사용해야 합니다. 이 예에서는 구성 요소 검색을 사용한다고 가정합니다.</p>
<p><code>save</code> 호출은 JPA 관점에서 꼭 필요한 것은 아니지만 Spring Data가 제공하는 저장소 추상화의 일관성을 유지하려면 여전히 존재해야 합니다.</p>
<h1 id="transactional-query-methods">Transactional query methods</h1>
<p>선언된 쿼리 메서드(기본 메서드 포함)에는 기본적으로 트랜잭션 구성이 적용되지 않습니다. 이러한 메서드를 트랜잭션 방식으로 실행하려면 다음 예제와 같이 정의한 저장소 인터페이스에서 <code>@Transactional</code>을 사용하세요.</p>
<p>Example 3. Using @Transactional at query methods</p>
<pre><code class="language-java">@Transactional(readOnly = true)
interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  List&lt;User&gt; findByLastname(String lastname);

  @Modifying
  @Transactional
  @Query(&quot;delete from User u where u.active = false&quot;)
  void deleteInactiveUsers();
}</code></pre>
<p>일반적으로 대부분의 쿼리 메서드는 데이터만 읽기 때문에 <code>readOnly</code> 플래그를 <code>true</code>로 설정하려고 합니다. 이와 대조적으로 <code>deleteInactiveUsers()</code>는 <code>@Modifying</code> annotation을 사용하고 트랜잭션 구성을 재정의합니다. 따라서 메서드는 <code>readOnly</code> 플래그가 <code>false</code>로 설정된 상태로 실행됩니다.</p>
<blockquote>
<p><strong>[Note]</strong>
읽기 전용 쿼리에 트랜잭션을 사용하고 <code>readOnly</code> 플래그를 설정하여 트랜잭션을 표시할 수 있습니다. 그러나 이렇게 해도 조작 쿼리를 트리거하지 않는지 확인하는 역할을 하지 않습니다(일부 데이터베이스는 읽기 전용 트랜잭션 내에서 <code>INSERT</code> 및 <code>UPDATE</code> 문을 거부하지만). 대신 <code>readOnly</code> 플래그는 성능 최적화를 위해 기본 JDBC 드라이버에 대한 힌트로 전파됩니다. 게다가 Spring은 기본 JPA 제공자에 대해 일부 최적화를 수행합니다. 예를 들어, Hibernate와 함께 사용될 때, 트랜잭션을 <code>readOnly</code>로 구성할 때 플러시(flush) 모드는 <code>NEVER</code>로 설정됩니다. 이는 Hibernate가 더티 검사를 건너뛰게 합니다(대형 객체 트리에서 눈에 띄는 개선).</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Query by Example]]></title>
            <link>https://velog.io/@dev_hammy/Query-by-Example</link>
            <guid>https://velog.io/@dev_hammy/Query-by-Example</guid>
            <pubDate>Sun, 21 Apr 2024 08:50:17 GMT</pubDate>
            <description><![CDATA[<h1 id="introduction">Introduction</h1>
<p>이 장에서는 Query by Example를 소개하고 사용 방법을 설명합니다.</p>
<p>QBE(Query by example)는 간단한 인터페이스를 갖춘 사용자 친화적인 쿼리 기술입니다. 동적 쿼리 생성이 가능하며 필드 이름이 포함된 쿼리를 작성할 필요가 없습니다. 실제로 Query by example에서는 store별 쿼리 언어를 사용하여 쿼리를 작성할 필요가 전혀 없습니다.</p>
<blockquote>
<p><strong>[Note]</strong>
이 장에서는 Query By Example의 핵심 개념을 설명합니다. 정보는 Spring Data Commons 모듈에서 가져옵니다. 데이터베이스에 따라 문자열 일치 지원이 제한될 수 있습니다.</p>
</blockquote>
<h1 id="usage">Usage</h1>
<p>QBE API는 네 부분으로 구성됩니다.</p>
<ul>
<li><p>Probe: 채워진 필드가 있는 도메인 객체의 실제 예입니다.</p>
</li>
<li><p><code>ExampleMatcher</code>: <code>ExampleMatcher</code>는 특정 필드를 일치시키는 방법에 대한 세부 정보를 전달합니다. 여러 예제에서 재사용할 수 있습니다.</p>
</li>
<li><p><code>Example</code> : <code>Example</code>는 프로브와 <code>ExampleMatcher</code>로 구성됩니다. 쿼리를 생성하는 데 사용됩니다.</p>
</li>
<li><p><code>FetchableFluentQuery</code>: <code>FetchableFluentQuery</code>는 <code>Example</code>에서 파생된 쿼리를 추가로 사용자 정의할 수 있는 Fetchable API를 제공합니다. Fluent API를 사용하면 쿼리에 대한 순서 예측 및 결과 처리를 지정할 수 있습니다.</p>
</li>
</ul>
<p>QBE는 여러 사용 사례에 매우 적합합니다.</p>
<ul>
<li><p>일련의 정적 또는 동적 제약 조건을 사용하여 데이터 store를 쿼리합니다.</p>
</li>
<li><p>기존 쿼리 중단에 대한 걱정 없이 도메인 개체를 자주 리팩터링합니다.</p>
</li>
<li><p>기본 데이터 store API와 독립적으로 작동합니다.</p>
</li>
</ul>
<p>사례별 쿼리에는 다음과 같은 몇 가지 제한 사항도 있습니다.</p>
<ul>
<li><p><code>firstname = ?0 or (fistname =?1 and lastname = ?2)</code>와 같은 중첩되거나 그룹화된 속성 제약 조건은 지원되지 않습니다.</p>
</li>
<li><p>문자열 일치에 대한 store별 지원. 데이터베이스에 따라 문자열 일치는 문자열에 대한 시작/포함/끝/정규식을 지원할 수 있습니다.</p>
</li>
<li><p>다른 property type과 정확히 일치합니다.</p>
</li>
</ul>
<p>사례별 쿼리를 시작하기 전에 도메인 개체가 있어야 합니다. 시작하려면 다음 예와 같이 repository에 대한 인터페이스를 생성하십시오.</p>
<p>Sample Person object</p>
<pre><code class="language-java">public class Person {

  @Id
  private String id;
  private String firstname;
  private String lastname;
  private Address address;

  // … getters and setters omitted
}</code></pre>
<p>앞의 예에서는 간단한 도메인 개체를 보여줍니다. 이를 사용하여 <code>Example</code>를 만들 수 있습니다. 기본적으로 <code>null</code> 값이 있는 필드는 무시되고 문자열은 store별 기본값을 사용하여 일치됩니다.</p>
<blockquote>
<p><strong>[Note]</strong>
Query by Example criteria 에 속성을 포함하는 것은 Null 허용 여부를 기반으로 합니다. 기본 유형(<code>int</code>, <code>double</code>, …)을 사용하는 속성은 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-by-example.html#query-by-example.matchers"><code>ExampleMatcher</code>가 속성 경로를 무시</a>하지 않는 한 항상 포함됩니다.</p>
</blockquote>
<p>Examples는 <code>of</code> 팩토리 메서드를 사용하거나 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-by-example.html#query-by-example.matchers"><code>ExampleMatcher</code></a>를 사용하여 빌드할 수 있습니다. <code>Example</code>는 변경할 수 없습니다(immutable). 다음 목록은 간단한 예를 보여줍니다.</p>
<p>Example 1. Simple Example</p>
<pre><code class="language-java">Person person = new Person();
person.setFirstname(&quot;Dave&quot;);

Example&lt;Person&gt; example = Example.of(person);</code></pre>
<p>(1) 도메인 개체의 새 인스턴스를 만듭니다. 
(2) 쿼리할 속성을 설정합니다. 
(3) Example를 생성합니다.</p>
<p>리포지토리를 사용하여 예제 쿼리를 실행할 수 있습니다. 이렇게 하려면 저장소 인터페이스가 <code>QueryByExampleExecutor&lt;T&gt;</code>를 확장하도록 하세요. 다음 목록은 <code>QueryByExampleExecutor</code> 인터페이스에서 발췌한 내용을 보여줍니다.</p>
<p>The QueryByExampleExecutor</p>
<pre><code class="language-java">public interface QueryByExampleExecutor&lt;T&gt; {

  &lt;S extends T&gt; S findOne(Example&lt;S&gt; example);

  &lt;S extends T&gt; Iterable&lt;S&gt; findAll(Example&lt;S&gt; example);

  // … more functionality omitted.
}</code></pre>
<h1 id="example-matchers">Example Matchers</h1>
<p>Examples는 기본 설정에만 국한되지 않습니다. 다음 예에 표시된 대로 <code>ExampleMatcher</code>를 사용하여 문자열 일치, null 처리 및 속성별 설정에 대한 고유한 기본값을 지정할 수 있습니다.</p>
<p>Example 2. Example matcher with customized matching</p>
<pre><code class="language-java">Person person = new Person(); // (1)
person.setFirstname(&quot;Dave&quot;); // (2)

ExampleMatcher matcher = ExampleMatcher.matching() // (3)
  .withIgnorePaths(&quot;lastname&quot;) // (4)
  .withIncludeNullValues() // (5)
  .withStringMatcher(StringMatcher.ENDING); // (6)

Example&lt;Person&gt; example = Example.of(person, matcher); // (7)</code></pre>
<p>(1) 도메인 개체의 새 인스턴스를 만듭니다.
(2) 속성을 설정합니다.
(3) 모든 값이 일치할 것으로 예상하는 <code>ExampleMatcher</code>를 만듭니다. 추가 구성 없이도 이 단계에서 사용할 수 있습니다.
(4) <code>lastname</code> 속성 경로를 무시하도록 새 <code>ExampleMatcher</code>를 생성합니다.
(5) <code>lastname</code> 속성 경로를 무시하고 null 값을 포함하도록 새 <code>ExampleMatcher</code>를 생성합니다.
(6) <code>lastname</code> 속성 경로를 무시하고, null 값을 포함하고, 접미사 문자열 일치를 수행하도록 새 <code>ExampleMatcher</code>를 생성합니다.
(7) 도메인 객체와 구성된 <code>ExampleMatcher</code>를 기반으로 새 <code>Example</code>를 생성합니다.</p>
<p>기본적으로 <code>ExampleMatcher</code>는 프로브에 설정된 모든 값이 일치할 것으로 예상합니다. 암시적으로 정의된 predicate 중 하나와 일치하는 결과를 얻으려면 <code>ExampleMatcher.matchingAny()</code>를 사용하세요.</p>
<p>개별 속성(예: &quot;firstname&quot; 및 &quot;lastname&quot; 또는 중첩 속성의 경우 &quot;address.city&quot;)에 대한 동작을 지정할 수 있습니다. 다음 예와 같이 일치 옵션과 대소문자 구분을 사용하여 조정할 수 있습니다.</p>
<p>Configuring matcher options</p>
<pre><code class="language-java">ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher(&quot;firstname&quot;, endsWith())
  .withMatcher(&quot;lastname&quot;, startsWith().ignoreCase());
}</code></pre>
<p>matcher 옵션을 구성하는 또 다른 방법은 람다(Java 8에 도입됨)를 사용하는 것입니다. 이 접근 방식은 implementor에게 matcher를 수정하도록 요청하는 콜백을 생성합니다. 구성 옵션이 matcher 인스턴스 내에 보관되므로 matcher를 반환할 필요가 없습니다. 다음 예에서는 람다를 사용하는 매처를 보여줍니다.</p>
<p>Configuring matcher options with lambdas</p>
<pre><code class="language-java">ExampleMatcher matcher = ExampleMatcher.matching()
  .withMatcher(&quot;firstname&quot;, match -&gt; match.endsWith())
  .withMatcher(&quot;firstname&quot;, match -&gt; match.startsWith());
}</code></pre>
<p><code>Example</code>에서 생성된 쿼리는 구성의 merged view를 사용합니다. 기본 matching 설정은 <code>ExampleMatcher</code> 수준에서 설정할 수 있으며, 개별 설정은 특정 속성 경로에 적용될 수 있습니다. <code>ExampleMatcher</code>에 설정된 설정은 명시적으로 정의되지 않는 한 속성 경로 설정에 의해 상속됩니다. 속성 패치의 설정은 기본 설정보다 우선 순위가 높습니다. 다음 표에서는 다양한 <code>ExampleMatcher</code> 설정의 범위를 설명합니다.</p>
<p>Table 1. Scope of ExampleMatcher settings</p>
<table class="tableblock frame-all grid-all stretch">
<caption class="title">표 1. <code>ExampleMatcher</code> 설정 범위</caption>
<colgroup>
<col style="width: 33.3333%;">
<col style="width: 66.6667%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">설정</th>
<th class="tableblock halign-left valign-top">범위</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Null 처리</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>ExampleMatcher</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">문자열 매칭</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>ExampleMatcher</code> 및 속성 경로</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">속성 무시</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">속성 경로</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">대소문자 구분</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>ExampleMatcher</code> 및 속성 경로</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">값 변환</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">속성 경로</p></td>
</tr>
</tbody>
</table>


<h1 id="fluent-api">Fluent API</h1>
<p><code>QueryByExampleExecutor</code>는 지금까지 언급하지 않은 메서드를 하나 더 제공합니다. <code>&lt;S extends T, R&gt; R findBy(Example&lt;S&gt; example, Function&lt;FluentQuery.FetchableFluentQuery&lt;S&gt;, R&gt; queryFunction)</code>. 다른 방법과 마찬가지로 <code>Example</code>에서 파생된 쿼리를 실행합니다. 그러나 두 번째 인수를 사용하면 동적으로 제어할 수 없는 실행 측면을 제어할 수 있습니다. 두 번째 인수에서 <code>FetchableFluentQuery</code>의 다양한 메서드를 호출하면 됩니다. <code>sortBy</code>를 사용하면 결과의 순서를 지정할 수 있습니다. <code>as</code>를 사용하면 결과를 변환하려는 유형을 지정할 수 있습니다. <code>project</code>는 쿼리된 속성을 제한합니다. <code>first</code>, <code>firstValue</code>, <code>one</code>, <code>oneValue</code>, <code>all</code>, <code>page</code>, <code>stream</code>, <code>count</code> 및 <code>presents</code>는 얻을 수 있는 결과의 종류와 예상보다 많은 결과를 사용할 수 있을 때 쿼리가 작동하는 방식을 정의합니다.</p>
<p>fluent API를 사용하여 lastname순으로 정렬된 잠재적으로 많은 결과 중 마지막 결과를 얻으세요.</p>
<pre><code class="language-java">Optional&lt;Person&gt; match = repository.findBy(example,
    q -&gt; q
        .sortBy(Sort.by(&quot;lastname&quot;).descending())
        .first()
);</code></pre>
<p>Spring Data JPA에서는 다음 예제와 같이 리포지토리와 함께 QBE를 사용할 수 있습니다.</p>
<p>Example 3. Query by Example using a Repository</p>
<pre><code class="language-java">public interface PersonRepository extends JpaRepository&lt;Person, String&gt; { … }

public class PersonService {

  @Autowired PersonRepository personRepository;

  public List&lt;Person&gt; findPeople(Person probe) {
    return personRepository.findAll(Example.of(probe));
  }
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
현재 <code>SingularAttribute</code> 속성만 속성 일치에 사용할 수 있습니다.</p>
</blockquote>
<p>속성 지정자는 속성 이름(예: <code>firstname</code>, <code>lastname</code>)을 허용합니다. 속성을 점(<code>address.city</code>)으로 연결하여 탐색할 수 있습니다. 일치하는 옵션과 대소문자 구분을 사용하여 조정할 수도 있습니다.</p>
<p>다음 표에서는 사용할 수 있는 다양한 <code>StringMatcher</code> 옵션과 이를 <code>firstname</code>이라는 필드에 사용한 결과를 보여줍니다.</p>
<p>Table 2. StringMatcher options</p>
<table class="tableblock frame-all grid-all stretch">
<caption class="title">표 2. <code>StringMatcher</code> 옵션</caption>
<colgroup>
<col style="width: 33.3333%;">
<col style="width: 66.6667%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">매칭</th>
<th class="tableblock halign-left valign-top">논리적 결과</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>DEFAULT</code> (대소문자 구분)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>firstname = ?0</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>DEFAULT</code> (대소문자 무시)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LOWER(firstname) = LOWER(?0)</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>EXACT</code> (대소문자 구분)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>firstname = ?0</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>EXACT</code> (대소문자 무시)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LOWER(firstname) = LOWER(?0)</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>STARTING</code> (대소문자 구분)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>firstname like ?0 + '%'</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>STARTING</code> (대소문자 무시)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LOWER(firstname) like LOWER(?0) + '%'</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>ENDING</code> (대소문자 구분)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>firstname like '%' + ?0</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>ENDING</code> (대소문자 무시)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LOWER(firstname) like '%' + LOWER(?0)</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CONTAINING</code> (대소문자 구분)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>firstname like '%' + ?0 + '%'</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>CONTAINING</code> (대소문자 무시)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LOWER(firstname) like '%' + LOWER(?0) + '%'</code></p></td>
</tr>
</tbody>
</table>


<blockquote>
<p><strong>[Note]</strong>
정규식 matching은 JPA에서 지원되지 않습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Specifications]]></title>
            <link>https://velog.io/@dev_hammy/Specifications</link>
            <guid>https://velog.io/@dev_hammy/Specifications</guid>
            <pubDate>Sun, 21 Apr 2024 08:02:26 GMT</pubDate>
            <description><![CDATA[<p>JPA 2에는 프로그래밍 방식으로 쿼리를 작성하는 데 사용할 수 있는 기준 API가 도입되었습니다. <code>criteria</code>을 작성하여 도메인 클래스에 대한 쿼리의 where 절을 정의합니다. 한 걸음 더 물러서면 이러한 기준은 JPA 기준 API 제약 조건에 의해 설명되는 엔터티에 대한 조건자로 간주될 수 있습니다.</p>
<p>Spring Data JPA는 Eric Evans의 저서 &quot;Domain Driven Design&quot;에서 사양 개념을 가져와 동일한 의미론을 따르고 JPA 기준 API를 사용하여 이러한 사양을 정의하는 API를 제공합니다. 사양을 지원하려면 다음과 같이 <code>JpaSpecificationExecutor</code> 인터페이스를 사용하여 저장소 인터페이스를 확장할 수 있습니다.</p>
<pre><code class="language-java">public interface CustomerRepository extends CrudRepository&lt;Customer, Long&gt;, JpaSpecificationExecutor&lt;Customer&gt; {
 …
}</code></pre>
<p>추가 인터페이스에는 다양한 방식으로 사양을 실행할 수 있는 메서드가 있습니다. 예를 들어 <code>findAll</code> 메소드는 다음 예와 같이 사양과 일치하는 모든 엔터티를 반환합니다.</p>
<pre><code class="language-java">List&lt;T&gt; findAll(Specification&lt;T&gt; spec);</code></pre>
<p><code>Specification</code> 인터페이스는 다음과 같이 정의됩니다.</p>
<pre><code class="language-java">public interface Specification&lt;T&gt; {
  Predicate toPredicate(Root&lt;T&gt; root, CriteriaQuery&lt;?&gt; query,
            CriteriaBuilder builder);
}</code></pre>
<p>사양은 다음 예에 표시된 것처럼 필요한 모든 조합에 대해 쿼리(메서드)를 선언할 필요 없이 <code>JpaRepository</code>와 결합하여 사용할 수 있는 엔터티 위에 확장 가능한 predicate 세트를 구축하는 데 쉽게 사용할 수 있습니다.</p>
<p>Example 1. Specifications for a Customer</p>
<pre><code class="language-java">public class CustomerSpecs {


  public static Specification&lt;Customer&gt; isLongTermCustomer() {
    return (root, query, builder) -&gt; {
      LocalDate date = LocalDate.now().minusYears(2);
      return builder.lessThan(root.get(Customer_.createdAt), date);
    };
  }

  public static Specification&lt;Customer&gt; hasSalesOfMoreThan(MonetaryAmount value) {
    return (root, query, builder) -&gt; {
      // build query here
    };
  }
}</code></pre>
<p><code>Customer_</code> 유형은 JPA Metamodel 생성기를 사용하여 생성된 메타모델 유형입니다(예제는 Hibernate 구현 문서 참조). 따라서 <code>Customer_.createdAt</code> 표현식은 <code>Customer</code>이 <code>Date</code> 유형의 <code>createAt</code> 속성을 가지고 있다고 가정합니다. 그 외에도 비즈니스 요구 사항 추상화 수준에 대한 몇 가지 기준을 표현하고 실행 가능한 <code>Specifications</code>을 만들었습니다. 따라서 클라이언트는 다음과 같이 <code>Specification</code>을 사용할 수 있습니다.</p>
<p>Example 2. Using a simple Specification</p>
<pre><code class="language-java">List&lt;Customer&gt; customers = customerRepository.findAll(isLongTermCustomer());</code></pre>
<p>이런 종류의 데이터 액세스에 대한 쿼리를 만들어 보는 것은 어떨까요? 단일 <code>Specification</code>을 사용하면 일반 쿼리 선언에 비해 많은 이점을 얻지 못합니다. <code>Specification</code>의 힘은 사양을 결합하여 새로운 <code>Specification</code> 개체를 만들 때 실제로 빛납니다. 다음과 유사한 표현식을 작성하기 위해 우리가 제공하는 <code>Specification</code>의 기본 method을 통해 이를 달성할 수 있습니다.</p>
<p>Example 3. Combined Specifications</p>
<pre><code class="language-java">MonetaryAmount amount = new MonetaryAmount(200.0, Currencies.DOLLAR);
List&lt;Customer&gt; customers = customerRepository.findAll(
  isLongTermCustomer().or(hasSalesOfMoreThan(amount)));</code></pre>
<p><code>Specification</code>은 <code>Specification</code> 인스턴스를 연결하고 결합하기 위한 몇 가지 &quot;접착 코드&quot; 기본 방법을 제공합니다. 이러한 방법을 사용하면 새로운 <code>Specification</code> 구현을 생성하고 이를 기존 구현과 결합하여 데이터 액세스 계층을 확장할 수 있습니다.</p>
<p>그리고 JPA 2.1에서는 <code>CriteriaBuilder</code> API에 <code>CriteriaDelete</code>가 도입되었습니다. 이는 <code>JpaSpecificationExecutor</code>의 <code>delete(Specification)</code> API를 통해 제공됩니다.</p>
<p>Example 4. Using a Specification to delete entries.</p>
<pre><code class="language-java">Specification&lt;User&gt; ageLessThan18 = (root, query, cb) -&gt; cb.lessThan(root.get(&quot;age&quot;).as(Integer.class), 18)

userRepository.delete(ageLessThan18);</code></pre>
<p><code>Specification</code>은 <code>age</code> 필드(정수로 변환)가 <code>18</code> 미만인 기준을 구축합니다. <code>userRepository</code>에 전달되면 JPA의 <code>CriteriaDelete</code> 기능을 사용하여 올바른 <code>DELETE</code> 작업을 생성합니다. 그런 다음 삭제된 엔터티 수를 반환합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Stored Procedures]]></title>
            <link>https://velog.io/@dev_hammy/Stored-Procedures</link>
            <guid>https://velog.io/@dev_hammy/Stored-Procedures</guid>
            <pubDate>Sun, 21 Apr 2024 07:53:42 GMT</pubDate>
            <description><![CDATA[<p>JPA 2.1 사양에는 JPA 기준 쿼리 API를 사용하여 저장 프로시저 호출에 대한 지원이 도입되었습니다. 리포지토리 메서드에서 저장 프로시저 메타데이터를 선언하기 위한 <code>@Procedure</code> annotation을 도입했습니다.</p>
<p>다음 예제에서는 다음 저장 프로시저를 사용합니다.</p>
<p>Example 1. The definition of the plus1inout procedure in HSQL DB.</p>
<pre><code class="language-sql">/;
DROP procedure IF EXISTS plus1inout
/;
CREATE procedure plus1inout (IN arg int, OUT res int)
BEGIN ATOMIC
 set res = arg + 1;
END
/;</code></pre>
<p>저장 프로시저의 메타데이터는 엔터티 유형에 대한 <code>NamedStoredProcedureQuery</code> annotation을 사용하여 구성할 수 있습니다.</p>
<p>Example 2. StoredProcedure metadata definitions on an entity.</p>
<pre><code class="language-java">@Entity
@NamedStoredProcedureQuery(name = &quot;User.plus1&quot;, procedureName = &quot;plus1inout&quot;, parameters = {
  @StoredProcedureParameter(mode = ParameterMode.IN, name = &quot;arg&quot;, type = Integer.class),
  @StoredProcedureParameter(mode = ParameterMode.OUT, name = &quot;res&quot;, type = Integer.class) })
public class User {}</code></pre>
<p><code>@NamedStoredProcedureQuery</code>에는 저장 프로시저에 대한 두 가지 다른 이름이 있습니다. <code>name</code>은 JPA가 사용하는 이름입니다. <code>procedureName</code>은 데이터베이스에 있는 저장 프로시저의 이름입니다.</p>
<p>다양한 방법으로 리포지토리 메서드에서 저장 프로시저를 참조할 수 있습니다. 호출할 저장 프로시저는 <code>@Procedure</code> annotation의 <code>value</code> 또는 <code>procedureName</code> 속성을 사용하여 직접 정의할 수 있습니다. 이는 데이터베이스의 저장 프로시저를 직접 참조하고 <code>@NamedStoredProcedureQuery</code>를 통한 모든 구성을 무시합니다.</p>
<p>또는 <code>@NamedStoredProcedureQuery.name</code> 속성을 <code>@Procedure.name</code> 속성으로 지정할 수도 있습니다. <code>value</code>, <code>procedureName</code>, <code>name</code>이 모두 구성되지 않은 경우 리포지토리 메서드의 이름이 <code>name</code> 속성으로 사용됩니다.</p>
<p>다음 예에서는 명시적으로 매핑된 프로시저를 참조하는 방법을 보여줍니다.</p>
<p>Example 3. Referencing explicitly mapped procedure with name &quot;plus1inout&quot; in database.</p>
<pre><code class="language-java">@Procedure(&quot;plus1inout&quot;)
Integer explicitlyNamedPlus1inout(Integer arg);</code></pre>
<p>다음 예제는 이전 예제와 동일하지만 <code>procedureName</code> 별칭을 사용합니다.
Example 4. Referencing implicitly mapped procedure with name &quot;plus1inout&quot; in database via procedureName alias.</p>
<pre><code class="language-java">@Procedure(procedureName = &quot;plus1inout&quot;)
Integer callPlus1InOut(Integer arg);</code></pre>
<p>다음은 이전 두 개와 동일하지만 명시적인 annotation 속성 대신 메서드 이름을 사용합니다.
Example 5. Referencing implicitly mapped named stored procedure &quot;User.plus1&quot; in EntityManager by using the method name.</p>
<pre><code class="language-java">@Procedure
Integer plus1inout(@Param(&quot;arg&quot;) Integer arg);</code></pre>
<p>다음 예에서는 <code>@NamedStoredProcedureQuery.name</code> 특성을 참조하여 저장 프로시저를 참조하는 방법을 보여줍니다.
Example 6. Referencing explicitly mapped named stored procedure &quot;User.plus1IO&quot; in EntityManager.</p>
<pre><code class="language-java">@Procedure(name = &quot;User.plus1IO&quot;)
Integer entityAnnotatedCustomNamedProcedurePlus1IO(@Param(&quot;arg&quot;) Integer arg);</code></pre>
<p>호출되는 저장 프로시저에 단일 출력 매개변수가 있는 경우 해당 매개변수가 메소드의 반환 값으로 반환될 수 있습니다. <code>@NamedStoredProcedureQuery</code> annotation에 지정된 여러 출력 매개변수가 있는 경우 해당 매개변수는 <code>@NamedStoredProcedureQuery</code> annotation에 지정된 매개변수 이름이 되는 키를 사용하여 <code>Map</code>으로 반환될 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Projections]]></title>
            <link>https://velog.io/@dev_hammy/Projections</link>
            <guid>https://velog.io/@dev_hammy/Projections</guid>
            <pubDate>Sun, 21 Apr 2024 07:43:56 GMT</pubDate>
            <description><![CDATA[<h1 id="projections">Projections</h1>
<p>Spring Data 쿼리 메소드는 일반적으로 저장소에서 관리하는 집계 루트의 하나 또는 여러 인스턴스를 반환합니다. 그러나 때로는 해당 유형의 특정 속성을 기반으로 프로젝션을 생성하는 것이 바람직할 수도 있습니다. Spring Data를 사용하면 전용 반환 유형을 모델링하여 관리되는 집계의 부분 보기를 보다 선택적으로 검색할 수 있습니다.</p>
<p>다음 예와 같은 저장소 및 집계 루트 유형을 상상해 보십시오.</p>
<p>A sample aggregate and repository</p>
<pre><code class="language-java">class Person {

  @Id UUID id;
  String firstname, lastname;
  Address address;

  static class Address {
    String zipCode, city, street;
  }
}

interface PersonRepository extends Repository&lt;Person, UUID&gt; {

  Collection&lt;Person&gt; findByLastname(String lastname);
}</code></pre>
<p>이제 사람의 이름 속성만 검색한다고 가정해 보겠습니다. 이를 달성하기 위해 Spring Data는 무엇을 제공합니까? 이 장의 나머지 부분에서는 그 질문에 답합니다.</p>
<h2 id="interface-based-projections">Interface-based Projections</h2>
<p>쿼리 결과를 name 특성으로만 제한하는 가장 쉬운 방법은 다음 예제와 같이 읽을 속성에 대한 접근자 메서드를 노출하는 인터페이스를 선언하는 것입니다.</p>
<p>A projection interface to retrieve a subset of attributes</p>
<pre><code class="language-java">interface NamesOnly {

  String getFirstname();
  String getLastname();
}</code></pre>
<p>여기서 중요한 점은 여기에 정의된 속성이 집계 루트의 속성과 정확히 일치한다는 것입니다. 이렇게 하면 다음과 같이 쿼리 메서드를 추가할 수 있습니다.</p>
<p>A repository using an interface based projection with a query method</p>
<pre><code class="language-java">interface PersonRepository extends Repository&lt;Person, UUID&gt; {

  Collection&lt;NamesOnly&gt; findByLastname(String lastname);
}</code></pre>
<p>쿼리 실행 엔진은 반환된 각 요소에 대해 런타임 시 해당 인터페이스의 프록시 인스턴스를 생성하고 노출된 메서드에 대한 호출을 대상 개체에 전달합니다.</p>
<blockquote>
<p><strong>[Note]</strong>
기본 메서드(예: <code>CrudRepository</code>, store 별 repository 인터페이스 또는 <code>Simple…Repository</code>에서 선언됨)를 재정의하는 메서드를 저장소에 선언하면 선언된 반환 유형에 관계없이 기본 메서드가 호출됩니다. 프로젝션에는 기본 메소드를 사용할 수 없으므로 호환 가능한 반환 유형을 사용해야 합니다. 일부 저장소 모듈은 <code>@Query</code> annotation을 지원하여 재정의된 기본 메서드를 쿼리 메서드로 변환한 다음 프로젝션을 반환하는 데 사용할 수 있습니다.</p>
</blockquote>
<p>투영은 재귀적으로 사용될 수 있습니다. <code>Address</code> 정보 중 일부도 포함하려면 다음 예제와 같이 이에 대한 프로젝션 인터페이스를 만들고 <code>getAddress()</code> 선언에서 해당 인터페이스를 반환합니다.</p>
<p>A projection interface to retrieve a subset of attributes</p>
<pre><code class="language-java">interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}</code></pre>
<p>메서드 호출 시 대상 인스턴스의 주소 속성을 가져와서 프로젝션 프록시에 래핑합니다.</p>
<h3 id="closed-projections">Closed Projections</h3>
<p>접근자 메서드가 모두 대상 집계의 속성과 일치하는 프로젝션 인터페이스는 닫힌 프로젝션으로 간주됩니다. 다음 예제(이 장의 앞부분에서도 사용함)는 닫힌 투영입니다.</p>
<p>A closed projection</p>
<pre><code class="language-java">interface NamesOnly {

  String getFirstname();
  String getLastname();
}</code></pre>
<p>폐쇄형 프로젝션을 사용하는 경우 Spring Data는 프로젝션 프록시를 지원하는 데 필요한 모든 속성을 알고 있으므로 쿼리 실행을 최적화할 수 있습니다. 이에 대한 자세한 내용은 참조 문서의 모듈별 부분을 참조하세요.</p>
<h3 id="open-projections">Open Projections</h3>
<p>프로젝션 인터페이스의 접근자 메서드는 다음 예제와 같이 @Value 주석을 사용하여 새 값을 계산하는 데에도 사용할 수 있습니다.</p>
<p>An Open Projection</p>
<pre><code class="language-java">interface NamesOnly {

  @Value(&quot;#{target.firstname + &#39; &#39; + target.lastname}&quot;)
  String getFullName();
  …
}</code></pre>
<p>투영을 뒷받침하는 집계 루트는 <code>target</code> 변수에서 사용할 수 있습니다. <code>@Value</code>를 사용하는 프로젝션 인터페이스는 개방형 프로젝션입니다. 이 경우 Spring Data는 쿼리 실행 최적화를 적용할 수 없습니다. SpEL 표현식이 집계 루트의 모든 속성을 사용할 수 있기 때문입니다.</p>
<p><code>@Value</code>에 사용되는 표현식은 너무 복잡해서는 안 됩니다. 즉, <code>String</code> 변수로 프로그래밍하는 것을 피하는 것이 좋습니다. 매우 간단한 표현식의 경우 다음 예제와 같이 기본 메서드(Java 8에 도입됨)를 사용하는 것이 한 가지 옵션일 수 있습니다.</p>
<p>A projection interface using a default method for custom logic</p>
<pre><code class="language-java">interface NamesOnly {

  String getFirstname();
  String getLastname();

  default String getFullName() {
    return getFirstname().concat(&quot; &quot;).concat(getLastname());
  }
}</code></pre>
<p>이 접근 방식을 사용하려면 프로젝션 인터페이스에 노출된 다른 접근자 메서드를 기반으로 논리를 구현할 수 있어야 합니다. 두 번째로 더 유연한 옵션은 다음 예제와 같이 Spring Bean에서 사용자 정의 로직을 구현한 후 SpEL 표현식에서 이를 호출하는 것입니다.</p>
<p>Sample Person object</p>
<pre><code class="language-java">@Component
class MyBean {

  String getFullName(Person person) {
    …
  }
}

interface NamesOnly {

  @Value(&quot;#{@myBean.getFullName(target)}&quot;)
  String getFullName();
  …
}</code></pre>
<p>SpEL 표현식이 어떻게 <code>myBean</code>을 참조하고 <code>getFullName(…)</code> 메소드를 호출하고 프로젝션 대상을 메소드 매개변수로 전달하는지 확인하십시오. SpEL 표현식 평가가 지원되는 메소드는 표현식에서 참조할 수 있는 메소드 매개변수를 사용할 수도 있습니다. 메소드 매개변수는 <code>args</code>라는 <code>Object</code> 배열을 통해 사용할 수 있습니다. 다음 예에서는 <code>args</code> 배열에서 메서드 매개변수를 가져오는 방법을 보여줍니다.</p>
<p>Sample Person object</p>
<pre><code class="language-java">interface NamesOnly {

  @Value(&quot;#{args[0] + &#39; &#39; + target.firstname + &#39;!&#39;}&quot;)
  String getSalutation(String prefix);
}</code></pre>
<p>다시 말하지만, 더 복잡한 표현식의 경우 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html#projections.interfaces.open.bean-reference">앞서</a> 설명한 대로 Spring 빈을 사용하고 표현식이 메서드를 호출하도록 해야 합니다.</p>
<h3 id="nullable-wrappers">Nullable Wrappers</h3>
<p>프로젝션 인터페이스의 Getter는 null 안전성 향상을 위해 nullable 래퍼를 사용할 수 있습니다. 현재 지원되는 래퍼 유형은 다음과 같습니다.</p>
<ul>
<li><code>java.util.Optional</code></li>
<li><code>com.google.common.base.Optional</code></li>
<li><code>scala.Option</code></li>
<li><code>io.vavr.control.Option</code></li>
</ul>
<p>A projection interface using nullable wrappers</p>
<pre><code class="language-java">interface NamesOnly {

  Optional&lt;String&gt; getFirstname();
}</code></pre>
<p>기본 프로젝션 값이 <code>null</code>이 아닌 경우 래퍼 유형의 현재 표현을 사용하여 값이 반환됩니다. 지원 값이 <code>null</code>인 경우 getter 메서드는 사용된 래퍼 유형의 빈 표현을 반환합니다.</p>
<h2 id="class-based-projectionsdtos">Class-based Projections(DTOs)</h2>
<p>프로젝션을 정의하는 또 다른 방법은 검색할 필드에 대한 속성을 보유하는 값 유형 DTO(데이터 전송 개체)를 사용하는 것입니다. 이러한 DTO 유형은 프록싱이 발생하지 않고 중첩된 프로젝션이 적용될 수 없다는 점을 제외하면 프로젝션 인터페이스가 사용되는 것과 정확히 동일한 방식으로 사용할 수 있습니다.</p>
<p>저장소가 로드할 필드를 제한하여 쿼리 실행을 최적화하는 경우 로드할 필드는 노출되는 생성자의 매개 변수 이름에서 결정됩니다.</p>
<p>다음 예에서는 프로젝션 DTO를 보여줍니다.</p>
<p>A projecting DTO</p>
<pre><code class="language-java">record NamesOnly(String firstname, String lastname) {
}</code></pre>
<p>Java 레코드는 값 의미 체계를 준수하므로 DTO 유형을 정의하는 데 이상적입니다. 모든 필드는 <code>private final</code> 필드이며 <code>equals(…)</code>/<code>hashCode()</code>/<code>toString()</code> 메서드가 자동으로 생성됩니다. 또는 프로젝션하려는 속성을 정의하는 모든 클래스를 사용할 수 있습니다.</p>
<h2 id="dynamic-projections">Dynamic Projections</h2>
<p>지금까지 우리는 프로젝션 유형을 컬렉션의 반환 유형 또는 요소 유형으로 사용했습니다. 그러나 호출 시 사용할 유형을 선택하여 동적으로 만들 수도 있습니다. 동적 프로젝션을 적용하려면 다음 예에 표시된 것과 같은 쿼리 메서드를 사용합니다.</p>
<p>A repository using a dynamic projection parameter</p>
<pre><code class="language-java">interface PersonRepository extends Repository&lt;Person, UUID&gt; {

  &lt;T&gt; Collection&lt;T&gt; findByLastname(String lastname, Class&lt;T&gt; type);
}</code></pre>
<p>이러한 방식으로 메서드를 사용하여 다음 예에 표시된 대로 투영을 적용하거나 있는 그대로 집계를 얻을 수 있습니다.</p>
<p>Using a repository with dynamic projections</p>
<pre><code class="language-java">void someMethod(PersonRepository people) {

  Collection&lt;Person&gt; aggregates =
    people.findByLastname(&quot;Matthews&quot;, Person.class);

  Collection&lt;NamesOnly&gt; aggregates =
    people.findByLastname(&quot;Matthews&quot;, NamesOnly.class);
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
<code>Class</code> 유형의 쿼리 매개변수가 동적 프로젝션 매개변수로 적합한지 여부를 검사합니다. 쿼리의 실제 반환 유형이 <code>Class</code> 매개변수의 일반 매개변수 유형과 동일한 경우 일치하는 <code>Class</code> 매개변수를 쿼리 또는 SpEL 표현식 내에서 사용할 수 없습니다. <code>Class</code> 매개변수를 쿼리 인수로 사용하려면 <code>Class&lt;?&gt;</code>와 같은 다른 generic 매개변수를 사용해야 합니다.</p>
</blockquote>
<blockquote>
<p><strong>[Note]</strong>
JPQL을 사용한 클래스 기반 프로젝션은 JPQL 표현식의 생성자 표현식으로 제한된다는 점에 유의하는 것이 중요합니다. <code>SELECT new com.example.NamesOnly(u.firstname, u.lastname) from User u</code>. (DTO 유형의 FQDN 사용법에 유의하세요!) 이 JPQL 표현식은 명명된 쿼리를 정의하는 <code>@Query</code> annotation에서도 사용할 수 있습니다. 그리고 클래스 기반 프로젝션은 기본 쿼리와 전혀 작동하지 않는다는 점을 지적하는 것이 중요합니다. 해결 방법으로 <code>ResultSetMapping</code> 또는 Hibernate 특정 <code>ResultTransformer</code>와 함께 명명된 쿼리를 사용할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Query Methods]]></title>
            <link>https://velog.io/@dev_hammy/JPA-Query-Methods</link>
            <guid>https://velog.io/@dev_hammy/JPA-Query-Methods</guid>
            <pubDate>Sat, 20 Apr 2024 11:00:59 GMT</pubDate>
            <description><![CDATA[<p>이 섹션에서는 Spring Data JPA를 사용하여 쿼리를 생성하는 다양한 방법을 설명합니다</p>
<h1 id="query-lookup-strategies">Query Lookup Strategies</h1>
<p>JPA 모듈은 쿼리를 문자열로 수동으로 정의하거나 메서드 이름에서 파생되도록 지원합니다.</p>
<p>조건자 <code>IsStartingWith</code>, <code>StartingWith</code>, <code>StartsWith</code>, <code>IsEndingWith</code>, <code>EndingWith</code>, <code>EndsWith</code>, <code>IsNotContaining</code>, <code>NotContaining</code>, <code>NotContains</code>, <code>IsContaining</code>, <code>Containing</code>, <code>Contains</code>가 포함된 파생 쿼리는 이러한 쿼리에 대한 해당 인수가 삭제(sanitize)됩니다. 즉, 인수에 실제로 <code>LIKE</code>에서 와일드카드로 인식되는 문자가 포함되어 있으면 이러한 문자는 이스케이프 처리되어 리터럴로만 일치합니다. 사용되는 이스케이프 문자는 <code>@EnableJpaRepositories</code> annotation의 <code>escapeCharacter</code>를 설정하여 구성할 수 있습니다. <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query.spel-expressions">SpEL 표현식 사용</a>과 비교해 보세요.</p>
<h2 id="declared-queries">Declared Queries</h2>
<p>메소드 이름에서 파생된 쿼리를 얻는 것은 매우 편리하지만 메소드 이름 구문 분석기가 사용하려는 키워드를 지원하지 않거나 메소드 이름이 불필요하게 보기 흉해지는 상황에 직면할 수 있습니다. 따라서 명명 규칙을 통해 JPA 명명된 쿼리를 사용하거나(자세한 내용은 <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.named-queries">JPA 명명된 쿼리 사용</a> 참조) 쿼리 메서드에 <code>@Query</code>로 annotation을 달 수 있습니다(자세한 내용은 <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.at-query"><code>@Query</code> 사용</a> 참조).</p>
<h1 id="query-creation">Query Creation</h1>
<p>일반적으로 JPA의 쿼리 생성 메커니즘은 <a href="https://docs.spring.io/spring-data/commons/reference/repositories/query-methods.html">쿼리 메서드</a>에 설명된 대로 작동합니다. 다음 예에서는 JPA 쿼리 메서드가 무엇으로 변환되는지 보여줍니다.</p>
<p>Example 1. Query creation from method names</p>
<blockquote>
</blockquote>
<pre><code class="language-java">public interface UserRepository extends Repository&lt;User, Long&gt; {
  List&lt;User&gt;  findByEmailAddressAndLastname(String  emailAddress, String lastname);
}</code></pre>
<blockquote>
<p>여기에서 JPA 기준 API를 사용하여 쿼리를 생성하지만 기본적으로 이는 다음 쿼리로 변환됩니다. <code>select u from User where u.emailAddress = ?1 and u.lastname = ?2</code>. Spring Data JPA는 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.query-methods.query-property-expressions">속성 표현식</a>에 설명된 대로 속성 검사를 수행하고 중첩된 속성을 탐색합니다.</p>
</blockquote>
<p>다음 표에서는 JPA에 지원되는 키워드와 해당 키워드가 포함된 메서드의 변환 내용을 설명합니다.</p>
<p>Table 1. Supported keywords inside method names</p>
<table class="tableblock frame-all grid-all fit-content">
<caption class="title">표 1. 메서드 이름 내에서 지원되는 키워드</caption>
<colgroup>
<col>
<col>
<col>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">키워드</th>
<th class="tableblock halign-left valign-top">예시</th>
<th class="tableblock halign-left valign-top">JPQL 조각</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Distinct</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findDistinctByLastnameAndFirstname</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>select distinct …​ where x.lastname = ?1 and x.firstname = ?2</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>And</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByLastnameAndFirstname</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.lastname = ?1 and x.firstname = ?2</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Or</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByLastnameOrFirstname</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.lastname = ?1 or x.firstname = ?2</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Is</code>, <code>Equals</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstname</code>,<code>findByFirstnameIs</code>,<code>findByFirstnameEquals</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.firstname = ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Between</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByStartDateBetween</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.startDate between ?1 and ?2</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LessThan</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeLessThan</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age &lt; ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>LessThanEqual</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeLessThanEqual</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age &lt;= ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>GreaterThan</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeGreaterThan</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age &gt; ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>GreaterThanEqual</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeGreaterThanEqual</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age &gt;= ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>After</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByStartDateAfter</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.startDate &gt; ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Before</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByStartDateBefore</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.startDate &lt; ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>IsNull</code>, <code>Null</code></p></td>
<td class="tableblock
 halign-left valign-top"><p class="tableblock"><code>findByAge(Is)Null</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age is null</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>IsNotNull</code>, <code>NotNull</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAge(Is)NotNull</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age not null</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Like</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstnameLike</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.firstname like ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>NotLike</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstnameNotLike</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.firstname not like ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>StartingWith</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstnameStartingWith</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.firstname like ?1</code> (매개변수가 <code>%</code>로 끝남)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>EndingWith</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstnameEndingWith</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.firstname like ?1</code> (매개변수가 <code>%</code>로 시작함)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Containing</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstnameContaining</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.firstname like ?1</code> (매개변수가 <code>%</code>로 감싸짐)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>OrderBy</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeOrderByLastnameDesc</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age = ?1 order by x.lastname desc</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Not</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByLastnameNot</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.lastname &lt;&gt; ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>In</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeIn(Collection&lt;Age&gt; ages)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age in ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>NotIn</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByAgeNotIn(Collection&lt;Age&gt; ages)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.age not in ?1</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>True</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByActiveTrue()</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.active = true</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>False</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByActiveFalse()</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where x.active = false</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>IgnoreCase</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findByFirstnameIgnoreCase</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>… where UPPER(x.firstname) = UPPER(?1)</code></p></td>
</tr>
</tbody>
</table>

<blockquote>
<p><strong>[Note]</strong>
<code>In</code> 및 <code>NotIn</code>은 또한 <code>Collection</code>의 하위 클래스와 배열 또는 가변 인수를 매개변수로 사용합니다. 동일한 논리 연산자의 다른 구문 버전에 대해서는 리포지토리 쿼리 키워드를 확인하세요.</p>
</blockquote>
<blockquote>
<p><strong>[Warning]</strong>
<code>DISTINCT</code>는 까다로울 수 있으며 항상 예상한 결과를 생성하지 못할 수도 있습니다. 예를 들어 <code>select distinct u from User u</code>하면 <code>select distinct u.lastname from User u</code>하는 것과 완전히 다른 결과가 생성됩니다. 첫 번째 경우에는 <code>User.id</code>를 포함하므로 아무것도 중복되지 않으므로 전체 테이블을 얻게 되며 이는 <code>User</code> 개체로 구성됩니다.</p>
<p>그러나 후자의 쿼리는 초점을 <code>User.lastname</code>으로 좁히고 해당 테이블의 고유한 성을 모두 찾습니다. 이는 또한 <code>List&lt;User&gt;</code> 결과 집합 대신 <code>List&lt;String&gt;</code> 결과 집합을 생성합니다.</p>
<p><code>countDistinctByLastname(String lastname)</code>도 예상치 못한 결과를 생성할 수 있습니다. Spring Data JPA는 <code>select count(distinct u.id) from User u where u.lastname = ?1</code>를 파생합니다. 다시 말하지만, <code>u.id</code>는 중복 항목에 도달하지 않으므로 이 쿼리는 바인딩 성을 가진 모든 사용자를 계산합니다. <code>countByLastname(String lastname)</code>과 동일합니다!</p>
<p>어쨌든 이 쿼리의 요점은 무엇입니까? 특정 성을 가진 사람의 수를 찾으려면? 해당 구속력 있는 성을 가진 고유한 사람의 수를 찾으려면? 고유한 성의 수를 찾으려면? (마지막 쿼리는 완전히 다른 쿼리입니다!) <code>distinct</code>를 사용하려면 쿼리를 직접 작성하고 원하는 정보를 가장 잘 캡처하기 위해 <code>@Query</code>를 사용해야 하는 경우도 있습니다. 결과 집합을 캡처하기 위해 프로젝션이 필요할 수도 있기 때문입니다.</p>
</blockquote>
<h2 id="annotation-based-configuration">Annotation-based Configuration</h2>
<p>annotation 기반 구성은 다른 구성 파일을 편집할 필요가 없어 유지 관리 노력이 줄어든다는 장점이 있습니다. 모든 새로운 쿼리 선언에 대해 도메인 클래스를 다시 컴파일해야 하므로 이러한 이점에 대한 비용을 지불합니다.</p>
<p>Example 2. Annotation-based named query configuration</p>
<pre><code class="language-java">@Entity
@NamedQuery(name = &quot;User.findByEmailAddress&quot;,
  query = &quot;select u from User u where u.emailAddress = ?1&quot;)
public class User {

}</code></pre>
<h1 id="using-jpa-named-queries">Using JPA Named Queries</h1>
<blockquote>
<p><strong>[Note]</strong>
예제에서는 <code>&lt;named-query /&gt;</code> 요소와 <code>@NamedQuery</code> annotation을 사용합니다. 이러한 구성 요소에 대한 쿼리는 JPA 쿼리 언어로 정의되어야 합니다. 물론 <code>&lt;named-native-query /&gt;</code>나 <code>@NamedNativeQuery</code>도 사용할 수 있습니다. 이러한 요소를 사용하면 데이터베이스 플랫폼 독립성을 상실하여 기본 SQL에서 쿼리를 정의할 수 있습니다.</p>
</blockquote>
<h2 id="xml-named-query-definition">XML Named Query Definition</h2>
<p>XML 구성을 사용하려면 클래스 경로의 <code>META-INF</code> 폴더에 있는 <code>orm.xml</code> JPA 구성 파일에 필요한 <code>&lt;named-query /&gt;</code> 요소를 추가하세요. 일부 정의된 명명 규칙을 사용하면 명명된 쿼리의 자동 호출이 활성화됩니다. 자세한 내용은 아래를 참조하세요.</p>
<p>예 3. XML 명명된 쿼리 구성</p>
<pre><code class="language-xml">&lt;named-query name=&quot;User.findByLastname&quot;&gt;
  &lt;query&gt;select u from User u where u.lastname = ?1&lt;/query&gt;
&lt;/named-query&gt;</code></pre>
<p>쿼리에는 런타임 시 쿼리를 해결하는 데 사용되는 특수 이름이 있습니다.</p>
<h2 id="declaring-interfaces">Declaring Interfaces</h2>
<p>이러한 명명된 쿼리를 허용하려면 다음과 같이 <code>UserRepositoryWithRewriter</code>를 지정합니다.</p>
<p>예 4. UserRepository의 쿼리 메서드 선언</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  List&lt;User&gt; findByLastname(String lastname);

  User findByEmailAddress(String emailAddress);
}</code></pre>
<p>Spring Data는 구성된 도메인 클래스의 간단한 이름으로 시작하고 점으로 구분된 메서드 이름이 뒤따르는 명명된 쿼리에 대한 이러한 메서드에 대한 호출을 해결하려고 시도합니다. 따라서 앞의 예에서는 메서드 이름에서 쿼리를 만드는 대신 이전에 정의한 명명된 쿼리를 사용합니다.</p>
<h1 id="using-query">Using <code>@Query</code></h1>
<p>명명된 쿼리를 사용하여 엔터티에 대한 쿼리를 선언하는 것은 유효한 접근 방식이며 적은 수의 쿼리에 대해 잘 작동합니다. 쿼리 자체는 이를 실행하는 Java 메서드에 연결되어 있으므로 실제로 도메인 클래스에 annotation을 추가하는 대신 Spring Data JPA <code>@Query</code> annotation을 사용하여 쿼리를 직접 바인딩할 수 있습니다. 이렇게 하면 지속성 관련 정보로부터 도메인 클래스가 해방되고 쿼리가 저장소 인터페이스에 같은 위치에 배치됩니다.</p>
<p>쿼리 메서드에 annotation이 달린 쿼리는 <code>@NamedQuery</code>를 사용하여 정의된 쿼리나 <code>orm.xml</code>에 선언된 명명된 쿼리보다 우선합니다.</p>
<p>다음 예에서는 <code>@Query</code> annotation을 사용하여 생성된 쿼리를 보여줍니다.</p>
<p>Example 5. Declare query at the query method using @Query</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query(&quot;select u from User u where u.emailAddress = ?1&quot;)
  User findByEmailAddress(String emailAddress);
}</code></pre>
<h2 id="applying-a-queryrewriter">Applying a QueryRewriter</h2>
<p>때로는 적용하려는 기능의 수에 관계없이 쿼리가 <code>EntityManager</code>로 전송되기 전에 Spring Data JPA가 쿼리에 원하는 모든 항목을 적용하도록 하는 것이 불가능해 보일 수 있습니다.</p>
<p>쿼리가 <code>EntityManager</code>로 전송되기 직전에 쿼리를 직접 확인하고 &quot;rewrite&quot; 수 있습니다. 즉, 마지막 순간에 어떤 변경이라도 할 수 있습니다.</p>
<p>예 6. @Query를 사용하여 QueryRewriter 선언</p>
<pre><code class="language-java">public interface MyRepository extends JpaRepository&lt;User, Long&gt; {

        @Query(value = &quot;select original_user_alias.* from SD_USER original_user_alias&quot;,
                nativeQuery = true,
                queryRewriter = MyQueryRewriter.class)
        List&lt;User&gt; findByNativeQuery(String param);

        @Query(value = &quot;select original_user_alias from User original_user_alias&quot;,
                queryRewriter = MyQueryRewriter.class)
        List&lt;User&gt; findByNonNativeQuery(String param);
}</code></pre>
<p>이 예에서는 동일한 QueryRewriter를 활용하는 기본(순수 SQL) 재작성과 JPQL 쿼리를 모두 보여줍니다. 이 시나리오에서 Spring Data JPA는 해당 유형의 애플리케이션 컨텍스트에 등록된 Bean을 찾습니다.</p>
<p>다음과 같이 쿼리 재작성을 작성할 수 있습니다.</p>
<p>Example 7. Example QueryRewriter</p>
<pre><code class="language-java">public class MyQueryRewriter implements QueryRewriter {

     @Override
     public String rewrite(String query, Sort sort) {
         return query.replaceAll(&quot;original_user_alias&quot;, &quot;rewritten_user_alias&quot;);
     }
}</code></pre>
<p>Spring Framework의 <code>@Component</code> 기반 annotation 중 하나를 적용하거나 <code>@Configuration</code> 클래스 내 <code>@Bean</code> 메서드의 일부로 포함하여 <code>QueryRewriter</code>가 애플리케이션 컨텍스트에 등록되었는지 확인해야 합니다.</p>
<p>또 다른 옵션은 저장소 자체가 인터페이스를 구현하도록 하는 것입니다.</p>
<p>Example 8. Repository that provides the QueryRewriter</p>
<pre><code class="language-java">public interface MyRepository extends JpaRepository&lt;User, Long&gt;, QueryRewriter {

        @Query(value = &quot;select original_user_alias.* from SD_USER original_user_alias&quot;,
                nativeQuery = true,
                queryRewriter = MyRepository.class)
        List&lt;User&gt; findByNativeQuery(String param);

        @Query(value = &quot;select original_user_alias from User original_user_alias&quot;,
                queryRewriter = MyRepository.class)
        List&lt;User&gt; findByNonNativeQuery(String param);

        @Override
        default String rewrite(String query, Sort sort) {
            return query.replaceAll(&quot;original_user_alias&quot;, &quot;rewritten_user_alias&quot;);
        }
}</code></pre>
<p><code>QueryRewriter</code>로 수행하는 작업에 따라 각각 애플리케이션 컨텍스트에 등록된 둘 이상을 갖는 것이 좋습니다.</p>
<blockquote>
<p><strong>[Note]</strong>
CDI 기반 환경에서 Spring Data JPA는 <code>BeanManager</code>에서 <code>QueryRewriter</code> 구현 인스턴스를 검색합니다.</p>
</blockquote>
<h2 id="using-advanced-like-expressions">Using Advanced <code>LIKE</code> Expressions</h2>
<p><code>@Query</code>를 사용하여 생성된 수동으로 정의된 쿼리에 대한 쿼리 실행 메커니즘을 사용하면 다음 예제와 같이 쿼리 정의 내에서 고급 <code>LIKE</code> 표현식을 정의할 수 있습니다.</p>
<p>Example 9. Advanced like expressions in @Query</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query(&quot;select u from User u where u.firstname like %?1&quot;)
  List&lt;User&gt; findByFirstnameEndsWith(String firstname);
}</code></pre>
<p>앞의 예에서는 <code>LIKE</code> 구분 기호 문자(<code>%</code>)가 인식되고 쿼리가 유효한 JPQL 쿼리로 변환됩니다(<code>%</code> 제거). 쿼리를 실행하면 메서드 호출에 전달된 매개 변수가 이전에 인식된 <code>LIKE</code> 패턴으로 보강됩니다.</p>
<h2 id="native-queries">Native Queries</h2>
<p><code>@Query</code> 어노테이션을 사용하면 다음 예와 같이 <code>nativeQuery</code> 플래그를 true로 설정하여 기본 쿼리를 실행할 수 있습니다.</p>
<p>예 10. @Query를 사용하여 쿼리 메서드에서 native query 선언</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query(value = &quot;SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1&quot;, nativeQuery = true)
  User findByEmailAddress(String emailAddress);
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
Spring Data JPA는 현재 네이티브 쿼리에 대한 동적 정렬을 지원하지 않습니다. 왜냐하면 선언된 실제 쿼리를 조작해야 하기 때문입니다. 이는 네이티브 SQL에 대해 안정적으로 수행할 수 없습니다. 그러나 다음 예와 같이 개수 쿼리를 직접 지정하여 페이지 매김에 기본 쿼리를 사용할 수 있습니다.</p>
</blockquote>
<p>예 11. @Query를 사용하여 쿼리 메서드에서 페이지 매김을 위한 기본 개수 쿼리를 선언합니다.</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query(value = &quot;SELECT * FROM USERS WHERE LASTNAME = ?1&quot;,
    countQuery = &quot;SELECT count(*) FROM USERS WHERE LASTNAME = ?1&quot;,
    nativeQuery = true)
  Page&lt;User&gt; findByLastname(String lastname, Pageable pageable);
}</code></pre>
<p>쿼리 복사본에 <code>.count</code> 접미사를 추가하면 명명된 기본 쿼리에서도 유사한 접근 방식이 작동합니다. 하지만 개수 쿼리에 대한 결과 집합 매핑을 등록해야 할 수도 있습니다.</p>
<h1 id="using-sort">Using Sort</h1>
<p><code>PageRequest</code>를 제공하거나 <code>Sort</code>를 직접 사용하여 정렬을 수행할 수 있습니다. <code>Sort</code>의 <code>Order</code> 인스턴스 내에서 실제로 사용되는 속성은 도메인 모델과 일치해야 합니다. 즉, 쿼리 내에서 사용되는 속성이나 별칭으로 확인되어야 합니다. JPQL은 이를 상태 필드 경로 표현식으로 정의합니다.</p>
<blockquote>
<p><strong>[Note]</strong>
참조할 수 없는 경로 표현식을 사용하면 <code>Exception</code>가 발생합니다.</p>
</blockquote>
<p>그러나 <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.query-methods.at-query"><code>@Query</code></a>와 함께 <code>Sort</code>를 사용하면 <code>ORDER BY</code> 절 내에 함수가 포함된 경로 확인되지 않은 <code>Order</code> 인스턴스를 몰래 가져올 수 있습니다. 이는 주어진 쿼리 문자열에 <code>Order</code>가 추가되기 때문에 가능합니다. 기본적으로 Spring Data JPA는 함수 호출이 포함된 <code>Order</code> 인스턴스를 거부하지만 <code>JpaSort.unsafe</code>를 사용하여 잠재적으로 안전하지 않은 순서를 추가할 수 있습니다.</p>
<p>다음 예제에서는 <code>JpaSort</code>의 안전하지 않은 옵션을 포함하여 <code>Sort</code> 및 <code>JpaSort</code>를 사용합니다.</p>
<p>Example 12. Using Sort and JpaSort</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query(&quot;select u from User u where u.lastname like ?1%&quot;)
  List&lt;User&gt; findByAndSort(String lastname, Sort sort);

  @Query(&quot;select u.id, LENGTH(u.firstname) as fn_len from User u where u.lastname like ?1%&quot;)
  List&lt;Object[]&gt; findByAsArrayAndSort(String lastname, Sort sort);
}

repo.findByAndSort(&quot;lannister&quot;, Sort.by(&quot;firstname&quot;)); // (1)
repo.findByAndSort(&quot;stark&quot;, Sort.by(&quot;LENGTH(firstname)&quot;)); // (2)
repo.findByAndSort(&quot;targaryen&quot;, JpaSort.unsafe(&quot;LENGTH(firstname)&quot;)); // (3)
repo.findByAsArrayAndSort(&quot;bolton&quot;, Sort.by(&quot;fn_len&quot;)); // (4)</code></pre>
<p>(1) 도메인 모델의 속성을 가리키는 유효한 낷 표현식입니다.
(2) 함수 호출을 포함하는 잘못된 Sort입니다. 예외가 발생합니다.
(3) 명시적으로 안전하지 않은 Order가 포함된 유효한 Sort입니다.
(4) 별칭이 지정된 함수를 가리키는 유효한 Sort 표현식입니다.</p>
<h1 id="scrolling-large-query-results">Scrolling Large Query Results</h1>
<p>대규모 데이터 세트로 작업할 때 <a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#repositories.scrolling">스크롤</a>은 모든 결과를 메모리에 로드하지 않고도 해당 결과를 효율적으로 처리하는 데 도움이 될 수 있습니다.</p>
<p>대규모 쿼리 결과를 사용할 수 있는 여러 가지 옵션이 있습니다.</p>
<ol>
<li><p><a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.paging-and-sorting">페이징</a>. 이전 장에서 <code>Pageable</code> 및 <code>PageRequest</code>에 대해 배웠습니다.</p>
</li>
<li><p><a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#repositories.scrolling.offset">오프셋 기반 스크롤</a>. 이는 총 결과 개수가 필요하지 않기 때문에 페이징보다 가벼운 변형입니다.</p>
</li>
<li><p><a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#repositories.scrolling.keyset">키셋-베이스 스크롤</a>. 이 방법은 <a href="https://use-the-index-luke.com/no-offset">데이터베이스 인덱스를 활용하여 오프셋 기반 결과 검색의 단점을 방지</a>합니다.</p>
</li>
</ol>
<p><a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#repositories.scrolling.guidance">특정 배치에 가장 적합한 method</a>에 대해 자세히 알아보세요.</p>
<p>쿼리 메서드, <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-by-example.html">Query-by-Example</a> 및 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/core-extensions.html#core.extensions.querydsl">Querydsl</a>과 함께 Scroll API를 사용할 수 있습니다.</p>
<blockquote>
<p><strong>[Note]</strong>
문자열 기반 쿼리 방법을 사용한 스크롤은 아직 지원되지 않습니다. 저장된 <code>@Procedure</code> 쿼리 메서드를 사용한 스크롤도 지원되지 않습니다.</p>
</blockquote>
<h1 id="using-named-parameters">Using Named Parameters</h1>
<p>기본적으로 Spring Data JPA는 이전 예제에서 설명한 대로 위치 기반 매개변수 바인딩을 사용합니다. 이로 인해 매개변수 위치와 관련하여 리팩토링할 때 쿼리 메서드에 약간의 오류가 발생하기 쉽습니다. 이 문제를 해결하려면 다음 예제와 같이 <code>@Param</code> annotation을 사용하여 메서드 매개 변수에 구체적인 이름을 지정하고 쿼리에 이름을 바인딩할 수 있습니다.</p>
<p>Example 13. Using named parameters</p>
<pre><code class="language-java">public interface UserRepository extends JpaRepository&lt;User, Long&gt; {

  @Query(&quot;select u from User u where u.firstname = :firstname or u.lastname = :lastname&quot;)
  User findByLastnameOrFirstname(@Param(&quot;lastname&quot;) String lastname,
                                 @Param(&quot;firstname&quot;) String firstname);
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
메소드 매개변수는 정의된 쿼리의 순서에 따라 전환됩니다.</p>
</blockquote>
<blockquote>
<p><strong>[Note]</strong>
버전 4부터 Spring은 <code>-parameters</code> 컴파일러 플래그를 기반으로 Java 8의 매개변수 이름 검색을 완벽하게 지원합니다. 디버그 정보 대신 빌드에서 이 플래그를 사용하면 명명된 매개변수에 대한 <code>@Param</code> annotation을 생략할 수 있습니다.</p>
</blockquote>
<h1 id="using-spel-expressions">Using SpEL Expressions</h1>
<p>Spring Data JPA 릴리스 1.4부터 <code>@Query</code>로 정의된 수동 정의 쿼리에서 제한된 SpEL 템플릿 표현식의 사용을 지원합니다. 쿼리가 실행되면 이러한 표현식은 미리 정의된 변수 집합에 대해 평가됩니다. Spring Data JPA는 <code>entityName</code>이라는 변수를 지원합니다. 사용법은 <code>select x from #{#entityName} x</code>하는 것입니다. 지정된 저장소와 연결된 도메인 유형의 <code>entityName</code>을 삽입합니다. <code>entityName</code>은 다음과 같이 확인됩니다. 도메인 type이 <code>@Entity</code> annotation에 이름 속성을 설정한 경우 해당 속성이 사용됩니다. 그렇지 않으면 도메인 유형의 단순 클래스 이름이 사용됩니다.</p>
<p>다음 예에서는 쿼리 메서드와 수동으로 정의된 쿼리를 사용하여 저장소 인터페이스를 정의하려는 쿼리 문자열의 <code>#{#entityName}</code> 표현식에 대한 한 가지 사용 사례를 보여줍니다.</p>
<p>Example 14. Using SpEL expressions in repository query methods - entityName</p>
<pre><code class="language-java">@Entity
public class User {

  @Id
  @GeneratedValue
  Long id;

  String lastname;
}

public interface UserRepository extends JpaRepository&lt;User,Long&gt; {

  @Query(&quot;select u from #{#entityName} u where u.lastname = ?1&quot;)
  List&lt;User&gt; findByLastname(String lastname);
}</code></pre>
<p><code>@Query</code> annotation의 쿼리 문자열에 실제 엔터티 이름을 명시하지 않으려면 <code>#{#entityName}</code> 변수를 사용할 수 있습니다.</p>
<blockquote>
<p><strong>[Name]</strong>
<code>@Entity</code> annotation을 사용하여 <code>entityName</code>을 사용자 정의할 수 있습니다. SpEL 표현식에는 <code>orm.xml</code>의 사용자 정의가 지원되지 않습니다.</p>
</blockquote>
<p>물론 쿼리 선언에서 <code>User</code>를 직접 사용할 수도 있지만 그렇게 하려면 쿼리도 변경해야 합니다. <code>#entityName</code>에 대한 참조는 향후 <code>User</code> 클래스를 다른 엔터티 이름으로 다시 매핑할 가능성을 선택합니다(예: <code>@Entity(name = &quot;MyUser&quot;)</code> 사용).</p>
<p>쿼리 문자열의 <code>#{#entityName}</code> 표현식에 대한 또 다른 사용 사례는 구체적인 도메인 type에 대한 generic 저장소 인터페이스를 사용하여 일반 저장소 인터페이스를 정의하려는 경우입니다. 구체적인 인터페이스에서 사용자 정의 쿼리 메서드 정의를 반복하지 않으려면 다음 예와 같이 generic 저장소 인터페이스의 <code>@Query</code> annotation 쿼리 문자열에 엔터티 이름 표현식을 사용할 수 있습니다.</p>
<p>Example 15. Using SpEL expressions in repository query methods - entityName with inheritance</p>
<pre><code class="language-java">@MappedSuperclass
public abstract class AbstractMappedType {
  …
  String attribute;
}

@Entity
public class ConcreteType extends AbstractMappedType { … }

@NoRepositoryBean
public interface MappedTypeRepository&lt;T extends AbstractMappedType&gt;
  extends Repository&lt;T, Long&gt; {

  @Query(&quot;select t from #{#entityName} t where t.attribute = ?1&quot;)
  List&lt;T&gt; findAllByAttribute(String attribute);
}

public interface ConcreteRepository
  extends MappedTypeRepository&lt;ConcreteType&gt; { … }</code></pre>
<p>앞의 예에서 <code>MappedTypeRepository</code> 인터페이스는 <code>AbstractMappedType</code>을 확장하는 몇 가지 도메인 유형에 대한 공통 상위 인터페이스입니다. 또한 generic 저장소 인터페이스의 인스턴스에 사용할 수 있는 일반 <code>findAllByAttribute(…)</code> 메서드를 정의합니다. 이제 <code>ConcreteRepository</code>에서 <code>findByAllAttribute(…)</code>를 호출하면 쿼리는 <code>select t from ConcreteType t where t.attribute = ?1</code> 됩니다.</p>
<p>인수를 조작하는 SpEL 표현식을 사용하여 메서드 인수를 조작할 수도 있습니다. 이러한 SpEL 표현식에서는 엔터티 이름을 사용할 수 없지만 인수는 사용할 수 있습니다. 다음 예에 설명된 것처럼 이름이나 색인으로 액세스할 수 있습니다.</p>
<p>Example 16. Using SpEL expressions in repository query methods - accessing arguments.</p>
<pre><code class="language-java">@Query(&quot;select u from User u where u.firstname = ?1 and u.firstname=?#{[0]} and u.emailAddress = ?#{principal.emailAddress}&quot;)
List&lt;User&gt; findByFirstnameAndCurrentUserWithCustomQuery(String firstname);</code></pre>
<p><code>like-</code> 조건의 경우 문자열 값 매개변수의 시작 또는 끝에 <code>%</code>를 추가하려는 경우가 많습니다. 이는 바인드 매개변수 표시자 또는 SpEL 표현식에 <code>%</code>를 추가하거나 접두사를 추가하여 수행할 수 있습니다. 다음 예제에서는 이를 보여줍니다.</p>
<p>Example 17. Using SpEL expressions in repository query methods - wildcard shortcut.</p>
<pre><code class="language-java">@Query(&quot;select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%&quot;)
List&lt;User&gt; findByLastnameWithSpelExpression(@Param(&quot;lastname&quot;) String lastname);</code></pre>
<p>안전하지 않은 소스에서 오는 값과 함께 <code>like-</code> 조건을 사용하는 경우 값은 와일드카드를 포함할 수 없도록 삭제되어야 하며 이를 통해 공격자가 가능한 것보다 더 많은 데이터를 선택할 수 있습니다. 이를 위해 SpEL 컨텍스트에서 <code>escape(String)</code> 메소드를 사용할 수 있습니다. 첫 번째 인수에 있는 <code>_</code> 및 <code>%</code>의 모든 인스턴스 앞에는 두 번째 인수의 단일 문자가 붙습니다. JPQL 및 표준 SQL에서 사용할 수 있는 <code>like</code> 표현식의 <code>escape</code> 절과 결합하여 바인드 매개변수를 쉽게 정리할 수 있습니다.</p>
<p>Example 18. Using SpEL expressions in repository query methods - sanitizing input values.</p>
<pre><code class="language-java">@Query(&quot;select u from User u where u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}&quot;)
List&lt;User&gt; findContainingEscaped(String namePart);</code></pre>
<p>저장소 인터페이스 <code>findContainingEscaped(&quot;Peter_&quot;)</code>에서 이 메소드 선언이 주어지면 <code>Peter_Parker</code>는 찾지만 <code>Peter Parker</code>는 찾지 않습니다. 사용되는 이스케이프 문자는 <code>@EnableJpaRepositories</code> 주석의 <code>escapeCharacter</code>를 설정하여 구성할 수 있습니다. SpEL 컨텍스트에서 사용할 수 있는 <code>escape(String)</code> 메소드는 SQL 및 JPQL 표준 와일드카드인 <code>_</code> 및 <code>%</code>만 이스케이프합니다. 기본 데이터베이스 또는 JPA 구현이 추가 와일드카드를 지원하는 경우 이러한 와일드카드는 이스케이프되지 않습니다.</p>
<h1 id="other-methods">Other Methods</h1>
<p>Spring Data JPA는 쿼리를 작성하는 다양한 방법을 제공합니다. 그러나 때로는 귀하의 쿼리가 제공된 기술에 비해 너무 복잡할 수도 있습니다. 그러한 상황에서는 다음을 고려하십시오.</p>
<ul>
<li><p>아직 작성하지 않았다면 <code>@Query</code>를 사용하여 직접 쿼리를 작성하세요.</p>
</li>
<li><p>이것이 귀하의 요구 사항에 맞지 않으면 맞춤 구현을 구현하는 것이 좋습니다. 이를 통해 구현을 완전히 사용자에게 맡기면서 저장소에 메소드를 등록할 수 있습니다. 이는 다음과 같은 기능을 제공합니다.</p>
<ul>
<li><p><code>EntityManager</code>와 직접 대화합니다(순수 HQL/JPQL/EQL/네이티브 SQL 작성 또는 Criteria API 사용).</p>
</li>
<li><p>Spring Framework의 <code>JdbcTemplate</code>(네이티브 SQL) 활용</p>
</li>
<li><p>다른 타사 데이터베이스 도구 키트를 사용하세요.</p>
</li>
</ul>
</li>
<li><p>또 다른 옵션은 쿼리를 데이터베이스 내부에 넣은 다음 Spring Data JPA의 <code>@StoredProcedure</code> annotation을 사용하거나 데이터베이스 함수인 경우 <code>@Query</code> annotation을 사용하고 <code>CALL</code>로 호출하는 것입니다.</p>
</li>
</ul>
<p>이러한 전술은 Spring Data JPA가 리소스 관리를 제공하도록 하면서 쿼리를 최대한 제어해야 할 때 가장 효과적일 수 있습니다.</p>
<h1 id="modifying-queries">Modifying Queries</h1>
<p>이전 섹션에서는 모두 특정 엔터티 또는 엔터티 컬렉션에 액세스하기 위한 쿼리를 선언하는 방법을 설명했습니다. <a href="https://docs.spring.io/spring-data/commons/reference/repositories/custom-implementations.html">Spring 데이터 저장소에 대한 사용자 정의 구현</a>에 설명된 사용자 정의 메소드 기능을 사용하여 사용자 정의 수정 동작을 추가할 수 있습니다. 이 접근 방식은 포괄적인 사용자 지정 기능에 적합하므로 다음 예와 같이 쿼리 메서드에 <code>@Modifying</code> annotation을 추가하여 매개 변수 바인딩만 필요한 쿼리를 수정할 수 있습니다.</p>
<p>Example 19. Declaring manipulating queries</p>
<pre><code class="language-java">@Modifying
@Query(&quot;update User u set u.firstname = ?1 where u.lastname = ?2&quot;)
int setFixedFirstnameFor(String firstname, String lastname);</code></pre>
<p>이렇게 하면 쿼리를 선택하는 대신 업데이트 쿼리로 메서드에 주석이 달린 쿼리가 트리거됩니다. 수정 쿼리 실행 후 <code>EntityManager</code>에 오래된 엔터티가 포함될 수 있으므로 이를 자동으로 지우지 않습니다(자세한 내용은 <code>EntityManager.clear()</code>의 JavaDoc 참조). 이는 <code>EntityManager</code>에 아직 보류 중인 플러시되지 않은 모든 변경 사항을 효과적으로 삭제하기 때문입니다. <code>EntityManager</code>를 자동으로 지우려면 <code>@Modifying</code> annotation의<code>clearAutomatically</code> 속성을 <code>true</code>로 설정하면 됩니다.</p>
<p><code>@Modifying</code> annotation은 <code>@Query</code> annotation과 결합된 경우에만 관련됩니다. 파생된 쿼리 메서드 또는 사용자 지정 메서드에는 이 annotation이 필요하지 않습니다.</p>
<h2 id="derived-delete-queries">Derived Delete Queries</h2>
<p>Spring Data JPA는 다음 예제와 같이 JPQL 쿼리를 명시적으로 선언하지 않아도 되도록 파생된 삭제 쿼리도 지원합니다.</p>
<p>Example 20. Using a derived delete query</p>
<pre><code class="language-java">interface UserRepository extends Repository&lt;User, Long&gt; {

  void deleteByRoleId(long roleId);

  @Modifying
  @Query(&quot;delete from User u where u.role.id = ?1&quot;)
  void deleteInBulkByRoleId(long roleId);
}</code></pre>
<p><code>deleteByRoleId(…)</code> 메서드는 기본적으로 <code>deleteInBulkByRoleId(…)</code>와 동일한 결과를 생성하는 것처럼 보이지만 실행 방식 측면에서 두 메서드 선언 사이에는 중요한 차이점이 있습니다. 이름에서 알 수 있듯이 후자의 방법은 데이터베이스에 대해 단일 JPQL 쿼리(annotation에 정의된 쿼리)를 실행합니다. 이는 현재 로드된 <code>User</code> 인스턴스라도 호출된 수명 주기 콜백을 볼 수 없음을 의미합니다.</p>
<p>수명 주기 쿼리가 실제로 호출되는지 확인하기 위해 <code>deleteByRoleId(…)</code>를 호출하면 쿼리를 실행한 다음 반환된 인스턴스를 하나씩 삭제하므로 지속성 공급자가 실제로 해당 엔터티에 대해 <code>@PreRemove</code> 콜백을 호출할 수 있습니다.</p>
<p>실제로 파생된 삭제 쿼리는 쿼리를 실행한 다음 결과에 대해 <code>CrudRepository.delete(Iterable&lt;User&gt; users)</code>를 호출하고 동작을 <code>CrudRepository</code>의 다른 <code>delete(…)</code> 메서드 구현과 동기화하는 바로 가기입니다.</p>
<h1 id="applying-query-hints">Applying Query Hints</h1>
<p>저장소 인터페이스에 선언된 쿼리에 JPA 쿼리 힌트를 적용하려면 <code>@QueryHints</code> annotation을 사용할 수 있습니다. 다음 예제와 같이 페이지 매김을 적용할 때 트리거되는 추가 카운트 쿼리에 적용되는 힌트를 잠재적으로 비활성화하려면 JPA <code>@QueryHint</code> annotation 배열과 부울 플래그를 사용합니다.</p>
<p>Example 21. Using QueryHints with a repository method</p>
<pre><code class="language-java">public interface UserRepository extends Repository&lt;User, Long&gt; {

  @QueryHints(value = { @QueryHint(name = &quot;name&quot;, value = &quot;value&quot;)},
              forCounting = false)
  Page&lt;User&gt; findByLastname(String lastname, Pageable pageable);
}</code></pre>
<p>이전 선언에서는 실제 쿼리에 대해 구성된 <code>@QueryHint</code>를 적용하지만 총 페이지 수를 계산하기 위해 트리거된 개수 쿼리에는 적용을 생략합니다.</p>
<h2 id="adding-comments-to-queries">Adding Comments to Queries</h2>
<p>때로는 데이터베이스 성능을 기반으로 쿼리를 디버깅해야 하는 경우도 있습니다. 데이터베이스 관리자가 보여주는 쿼리는 <code>@Query</code>를 사용하여 작성한 쿼리와 매우 다르게 보일 수도 있고, 사용자 정의 파인더와 관련하여 Spring Data JPA가 생성했다고 가정하는 쿼리 또는 예제로 쿼리를 사용한 것과 전혀 다르게 보일 수도 있습니다.</p>
<p>이 프로세스를 더 쉽게 만들기 위해 <code>@Meta</code> annotation을 적용하여 쿼리든 다른 작업이든 거의 모든 JPA 작업에 사용자 정의 annotation을 삽입할 수 있습니다.</p>
<p>Example 22. Apply @Meta annotation to repository operations</p>
<pre><code class="language-java">public interface RoleRepository extends JpaRepository&lt;Role, Integer&gt; {

    @Meta(comment = &quot;find roles by name&quot;)
    List&lt;Role&gt; findByName(String name);

    @Override
    @Meta(comment = &quot;find roles using QBE&quot;)
    &lt;S extends Role&gt; List&lt;S&gt; findAll(Example&lt;S&gt; example);

    @Meta(comment = &quot;count roles for a given name&quot;)
    long countByName(String name);

    @Override
    @Meta(comment = &quot;exists based on QBE&quot;)
    &lt;S extends Role&gt; boolean exists(Example&lt;S&gt; example);
}</code></pre>
<p>이 샘플 저장소에는 <code>JpaRepository</code>에서 상속된 작업을 재정의할 뿐만 아니라 사용자 정의 파인더가 혼합되어 있습니다. 어느 쪽이든 <code>@Meta</code> annotation을 사용하면 쿼리가 데이터베이스로 전송되기 전에 쿼리에 삽입될 <code>comment</code>을 추가할 수 있습니다.</p>
<p>이 기능은 쿼리에만 국한되지 않는다는 점도 중요합니다. 이것은 <code>count</code>와 <code>exists</code> 작업까지 확장됩니다. 표시되지는 않지만 특정 <code>delete</code> 작업까지 확장됩니다.</p>
<blockquote>
<p><strong>[Important]</strong>
가능한 모든 곳에 이 기능을 적용하려고 시도했지만 기본 <code>EntityManager</code>의 일부 작업은 annotation을 지원하지 않습니다. 예를 들어, <code>entityManager.createQuery()</code>는 지원 annotaion으로 명확하게 문서화되어 있지만 <code>entityManager.find()</code> 작업은 그렇지 않습니다.</p>
</blockquote>
<p>JPQL 로깅이나 SQL 로깅은 JPA의 표준이 아니므로 아래 섹션에 표시된 것처럼 각 공급자에는 사용자 지정 구성이 필요합니다.</p>
<h3 id="activating-hibernate-comments">Activating Hibernate comments</h3>
<p>Hibernate에서 쿼리 comments을 활성화하려면 <code>hibernate.use_sql_comments</code>를 <code>true</code>로 설정해야 합니다. Java 기반 구성 설정을 사용하는 경우 다음과 같이 수행할 수 있습니다.</p>
<p>Example 23. Java-based JPA configuration</p>
<pre><code class="language-java">@Bean
public Properties jpaProperties() {

    Properties properties = new Properties();
    properties.setProperty(&quot;hibernate.use_sql_comments&quot;, &quot;true&quot;);
    return properties;
}</code></pre>
<p><code>persistence.xml</code> 파일이 있는 경우 해당 파일에 적용할 수 있습니다.</p>
<pre><code class="language-xml">&lt;persistence-unit name=&quot;my-persistence-unit&quot;&gt;

   ...registered classes...

    &lt;properties&gt;
        &lt;property name=&quot;hibernate.use_sql_comments&quot; value=&quot;true&quot; /&gt;
    &lt;/properties&gt;
&lt;/persistence-unit&gt;</code></pre>
<p>마지막으로 Spring Boot를 사용하는 경우 <code>application.properties</code> 파일 내에서 이를 설정할 수 있습니다.</p>
<p>Example 25. Spring Boot property-based configuration</p>
<pre><code class="language-properties">spring.jpa.properties.hibernate.use_sql_comments=true</code></pre>
<h3 id="activating-eclipselink-comments">Activating EclipseLink comments</h3>
<p>EclipseLink에서 쿼리 주석을 활성화하려면 <code>eclipselink.logging.level.sql</code>을 <code>FINE</code>으로 설정해야 합니다. Java 기반 구성 설정을 사용하는 경우 다음과 같이 수행할 수 있습니다.</p>
<p>Example 26. Java-based JPA configuration</p>
<pre><code class="language-java">@Bean
public Properties jpaProperties() {

    Properties properties = new Properties();
    properties.setProperty(&quot;eclipselink.logging.level.sql&quot;, &quot;FINE&quot;);
    return properties;
}</code></pre>
<p><code>persistence.xml</code> 파일이 있는 경우 해당 파일에 적용할 수 있습니다.</p>
<pre><code class="language-xml">&lt;persistence-unit name=&quot;my-persistence-unit&quot;&gt;

   ...registered classes...

    &lt;properties&gt;
        &lt;property name=&quot;eclipselink.logging.level.sql&quot; value=&quot;FINE&quot; /&gt;
    &lt;/properties&gt;
&lt;/persistence-unit&gt;</code></pre>
<p>마지막으로 Spring Boot를 사용하는 경우 application.properties 파일 내에서 이를 설정할 수 있습니다.</p>
<p>Example 28. Spring Boot property-based configuration</p>
<pre><code class="language-properties">spring.jpa.properties.eclipselink.logging.level.sql=FINE</code></pre>
<h1 id="configuring-fetch--and-loadgraphs">Configuring Fetch- and LoadGraphs</h1>
<p>JPA 2.1 사양에는 <code>@NamedEntityGraph</code> 정의를 참조할 수 있는 <code>@EntityGraph</code> annotation을 통해 지원하는 Fetch 및 LoadGraph 지정에 대한 지원이 도입되었습니다. 엔터티에서 해당 annotation을 사용하여 결과 쿼리의 가져오기 계획을 구성할 수 있습니다. 가져오기 type(<code>Fetch</code> 또는 <code>Locad</code>)은 <code>@EntityGraph</code> annotation의 유형 속성을 사용하여 구성할 수 있습니다. 자세한 내용은 JPA 2.1 Spec 3.7.4를 참조하세요.</p>
<p>다음 예에서는 엔터티에 명명된 엔터티 그래프를 정의하는 방법을 보여줍니다.</p>
<p>Example 29. Defining a named entity graph on an entity.</p>
<pre><code class="language-java">@Entity
@NamedEntityGraph(name = &quot;GroupInfo.detail&quot;,
  attributeNodes = @NamedAttributeNode(&quot;members&quot;))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List&lt;GroupMember&gt; members = new ArrayList&lt;GroupMember&gt;();

  …
}</code></pre>
<p>다음 예에서는 리포지토리 쿼리 메서드에서 명명된 엔터티 그래프를 참조하는 방법을 보여줍니다.</p>
<p>Example 30. Referencing a named entity graph definition on a repository query method.</p>
<pre><code class="language-java">public interface GroupRepository extends CrudRepository&lt;GroupInfo, String&gt; {

  @EntityGraph(value = &quot;GroupInfo.detail&quot;, type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}</code></pre>
<p><code>@EntityGraph</code>를 사용하여 임시 엔터티 그래프를 정의하는 것도 가능합니다. 다음 예제와 같이 <code>@NamedEntityGraph</code>를 도메인 유형에 명시적으로 추가할 필요 없이 제공된 <code>attributePaths</code>가 해당 <code>EntityGraph</code>로 변환됩니다.</p>
<p>Example 31. Using AD-HOC entity graph definition on an repository query method.</p>
<pre><code class="language-java">public interface GroupRepository extends CrudRepository&lt;GroupInfo, String&gt; {

  @EntityGraph(attributePaths = { &quot;members&quot; })
  GroupInfo getByGroupName(String name);

}</code></pre>
<h1 id="scrolling">Scrolling</h1>
<p>스크롤은 더 큰 결과 세트 청크를 반복하는 보다 세분화된 접근 방식입니다. 스크롤링은 안정적인 정렬, 스크롤 유형(오프셋 또는 키셋 기반 스크롤링) 및 결과 제한으로 구성됩니다. 속성 이름을 사용하여 간단한 정렬 식을 정의하고 쿼리 파생을 통해 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.limit-query-result"><code>Top</code> 또는 <code>First</code> 키워드</a>를 사용하여 정적 결과 제한을 정의할 수 있습니다. 표현식을 연결하여 여러 기준을 하나의 표현식으로 수집할 수 있습니다.</p>
<p>스크롤 쿼리는 애플리케이션이 전체 쿼리 결과를 사용할 때까지 다음 <code>Window&lt;T&gt;</code>를 얻기 위해 스크롤 위치를 다시 얻을 수 있는 <code>Window&lt;T&gt;</code>를 반환합니다. 다음 결과 배치를 획득하여 Java <code>Iterator&lt;List&lt;…&gt;&gt;</code>를 사용하는 것과 유사하게 쿼리 결과 스크롤을 사용하면 <code>Window.positionAt(…​)</code>를 통해 <code>ScrollPosition</code>에 액세스할 수 있습니다.</p>
<pre><code class="language-java">Window&lt;User&gt; users = repository.findFirst10ByLastnameOrderByFirstname(&quot;Doe&quot;, ScrollPosition.offset());
do {

  for (User u : users) {
    // consume the user
  }

  // obtain the next Scroll
  users = repository.findFirst10ByLastnameOrderByFirstname(&quot;Doe&quot;, users.positionAt(users.size() - 1));
} while (!users.isEmpty() &amp;&amp; users.hasNext());</code></pre>
<p><code>WindowIterator</code>는 다음 <code>Window</code>가 있는지 확인하고 <code>ScrollPosition</code>을 적용할 필요를 없애 <code>Windows</code> 전체에서 스크롤을 단순화하는 유틸리티를 제공합니다.</p>
<pre><code class="language-java">WindowIterator&lt;User&gt; users = WindowIterator.of(position -&gt; repository.findFirst10ByLastnameOrderByFirstname(&quot;Doe&quot;, position))
  .startingAt(OffsetScrollPosition.initial());

while (users.hasNext()) {
  User u = users.next();
  // consume the user
}</code></pre>
<h2 id="scrolling-using-offset">Scrolling using Offset</h2>
<p>오프셋 스크롤은 페이지 매김과 유사한 오프셋 카운터를 사용하여 여러 결과를 건너뛰고 데이터 소스가 지정된 오프셋에서 시작하는 결과만 반환하도록 합니다. 이 간단한 메커니즘은 큰 결과가 클라이언트 애플리케이션으로 전송되는 것을 방지합니다. 그러나 대부분의 데이터베이스에서는 서버가 결과를 반환하기 전에 전체 쿼리 결과를 구체화해야 합니다.</p>
<p>Example 32. Using OffsetScrollPosition with Repository Query Methods</p>
<pre><code class="language-java">interface UserRepository extends Repository&lt;User, Long&gt; {

  Window&lt;User&gt; findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}

WindowIterator&lt;User&gt; users = WindowIterator.of(position -&gt; repository.findFirst10ByLastnameOrderByFirstname(&quot;Doe&quot;, position))
  .startingAt(OffsetScrollPosition.initial()); // (1)</code></pre>
<p>(1) 위치 0의 초기 오프셋부터 시작합니다.</p>
<h2 id="scrolling-using-keyset-filtering">Scrolling using Keyset-Filtering</h2>
<p>오프셋 기반을 사용하려면 대부분의 데이터베이스에서 서버가 결과를 반환하기 전에 전체 결과를 구체화해야 합니다. 따라서 클라이언트는 요청된 결과 중 일부만 볼 수 있지만 서버는 전체 결과를 빌드해야 하므로 추가 로드가 발생합니다.</p>
<p>키 집합 필터링 접근 방식은 개별 쿼리에 대한 계산 및 I/O 요구 사항을 줄이는 것을 목표로 데이터베이스에 내장된 기능을 활용하여 하위 집합을 검색합니다. 이 접근 방식은 키를 쿼리에 전달하여 스크롤을 재개하는 키 세트를 유지 관리하고 필터 기준을 효과적으로 수정합니다.</p>
<p>Keyset-Filtering의 핵심 아이디어는 안정적인 정렬 순서를 사용하여 결과 검색을 시작하는 것입니다. 다음 청크로 스크롤하려면 정렬된 결과 내에서 위치를 재구성하는 데 사용되는 <code>ScrollPosition</code>을 얻습니다. <code>ScrollPosition</code>은 현재 <code>Window</code> 내 마지막 엔터티의 키 세트를 캡처합니다. 쿼리를 실행하기 위해 재구성에서는 데이터베이스가 잠재적인 인덱스를 활용하여 쿼리를 실행할 수 있도록 모든 정렬 필드와 기본 키를 포함하도록 기준 절을 다시 작성합니다. 데이터베이스는 큰 결과를 완전히 구체화한 다음 특정 오프셋에 도달할 때까지 결과를 건너뛸 필요 없이 지정된 키 세트 위치에서 훨씬 작은 결과만 구성하면 됩니다.</p>
<blockquote>
<p><strong>[Warning]</strong>
키 세트 필터링에서는 키 세트 속성(정렬에 사용되는 속성)이 <code>null</code>을 허용하지 않아야 합니다. 이 제한은 비교 연산자의 저장소별 null 값 처리와 인덱싱된 소스에 대해 쿼리를 실행해야 하기 때문에 적용됩니다. Null 허용 속성에 대한 키 집합 필터링은 예상치 못한 결과를 초래합니다.</p>
</blockquote>
<p>Using KeysetScrollPosition with Repository Query Methods</p>
<pre><code class="language-java">interface UserRepository extends Repository&lt;User, Long&gt; {

  Window&lt;User&gt; findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}

WindowIterator&lt;User&gt; users = WindowIterator.of(position -&gt; repository.findFirst10ByLastnameOrderByFirstname(&quot;Doe&quot;, position))
  .startingAt(ScrollPosition.keyset()); // (1) </code></pre>
<p>(1) 처음부터 시작하고 추가 필터링을 적용하지 마십시오.</p>
<p>키 집합 필터링은 데이터베이스에 정렬 필드와 일치하는 인덱스가 포함되어 있을 때 가장 잘 작동하므로 정적 정렬이 잘 작동합니다. Keyset-Filtering을 적용한 스크롤 쿼리는 정렬 순서에 사용된 속성이 쿼리에 의해 반환되어야 하며, 이러한 속성은 반환된 엔터티에 매핑되어야 합니다.</p>
<p>인터페이스 및 DTO 프로젝션을 사용할 수 있지만 키 세트 추출 실패를 방지하려면 정렬한 모든 속성을 포함해야 합니다.</p>
<p><code>Sort</code> 순서를 지정할 때 쿼리와 관련된 정렬 속성을 포함하면 충분합니다. 원하지 않는 경우 고유한 쿼리 결과를 보장할 필요가 없습니다. 키 세트 쿼리 메커니즘은 각 쿼리 결과가 고유하도록 기본 키(또는 나머지 복합 기본 키)를 포함하여 정렬 순서를 수정합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Defining Query Methods]]></title>
            <link>https://velog.io/@dev_hammy/Defining-Query-Methods</link>
            <guid>https://velog.io/@dev_hammy/Defining-Query-Methods</guid>
            <pubDate>Fri, 19 Apr 2024 17:16:07 GMT</pubDate>
            <description><![CDATA[<p>repository 프록시에는 메소드 이름에서 store별 쿼리를 파생시키는 두 가지 방법이 있습니다.</p>
<ul>
<li><p>메소드 이름에서 직접 쿼리를 파생합니다.</p>
</li>
<li><p>수동으로 정의된 쿼리를 사용합니다.</p>
</li>
</ul>
<p>사용 가능한 옵션은 실제 store에 따라 다릅니다. 그러나 실제 쿼리가 무엇인지 결정하는 전략이 있어야 합니다. 다음 섹션에서는 사용 가능한 옵션에 대해 설명합니다.</p>
<h1 id="query-lookup-strategies">Query Lookup Strategies</h1>
<p>쿼리를 해결하기 위해 리포지토리 인프라에 다음 전략을 사용할 수 있습니다. XML 구성을 사용하면 <code>query-lookup-strategy</code> 속성(attribute)을 통해 네임스페이스에서 전략을 구성할 수 있습니다. Java 구성의 경우 <code>EnableJpaRepositories</code> annotation의 <code>queryLookupStrategy</code> 속성(attribute)을 사용할 수 있습니다. 특정 데이터스토어에서는 일부 전략이 지원되지 않을 수 있습니다.</p>
<ul>
<li><p><code>CREATE</code>는 쿼리 메서드 이름에서 store별 쿼리를 생성하려고 시도합니다. 일반적인 접근 방식은 메소드 이름에서 잘 알려진 접두사 세트를 제거하고 메소드의 나머지 부분을 구문 분석하는 것입니다. 쿼리 생성에 대한 자세한 내용은 &quot;<a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.query-methods.query-creation">쿼리 생성</a>&quot;에서 확인할 수 있습니다.</p>
</li>
<li><p><code>USE_DECLARED_QUERY</code> 는 선언된 쿼리를 찾으려고 시도하고 쿼리를 찾을 수 없으면 예외를 발생시킵니다. 쿼리는 어딘가에 annotation으로 정의되거나 다른 방법으로 선언될 수 있습니다. 해당 store에 사용 가능한 옵션을 찾으려면 해당 store의 설명서를 참조하세요. repository 인프라가 부트스트랩 시 메서드에 대해 선언된 쿼리를 찾지 못하면 실패합니다.</p>
</li>
<li><p><code>CREATE_IF_NOT_FOUND</code>(기본값)는 <code>CREATE</code>와 <code>USE_DECLARED_QUERY</code>를 결합합니다. 선언된 쿼리를 먼저 조회하고, 선언된 쿼리가 없으면 사용자 지정 메서드 이름 기반 쿼리를 생성합니다. 이는 기본 조회 전략이므로 명시적으로 아무것도 구성하지 않은 경우에 사용됩니다. 메소드 이름으로 쿼리를 빠르게 정의할 수 있을 뿐만 아니라 필요에 따라 선언된 쿼리를 도입하여 이러한 쿼리를 사용자 정의할 수도 있습니다.</p>
</li>
</ul>
<h1 id="query-creation">Query Creation</h1>
<p>Spring Data 저장소 인프라에 내장된 쿼리 빌더 메커니즘은 저장소의 엔터티에 대한 제한 쿼리를 작성하는 데 유용합니다. 다음 예에서는 여러 쿼리를 만드는 방법을 보여줍니다.</p>
<p>메소드 이름에서 쿼리 생성</p>
<pre><code class="language-java">interface PersonRepository extends Repository&lt;Person, Long&gt; {

  List&lt;Person&gt; findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List&lt;Person&gt; findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List&lt;Person&gt; findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List&lt;Person&gt; findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List&lt;Person&gt; findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List&lt;Person&gt; findByLastnameOrderByFirstnameAsc(String lastname);
  List&lt;Person&gt; findByLastnameOrderByFirstnameDesc(String lastname);
}</code></pre>
<p>파싱 쿼리 메소드 이름은 주어(Subject)와 술어(Predicate)로 구분됩니다. 첫 번째 부분(<code>find…By</code>, <code>exists…By</code>)은 쿼리 subject을 정의하고 두 번째 부분은 predicate를 구성합니다. 도입절(subject)에는 추가 표현이 포함될 수 있습니다. <code>find</code>(또는 기타 도입 키워드)와 <code>By</code> 사이의 모든 텍스트는 생성될 쿼리에 고유한 플래그를 설정하기 위해 <code>Distinct</code>나 쿼리 결과를 제한하기 위해 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.limit-query-result"><code>Top</code>/<code>First</code>와 같은 결과 제한 키워드</a> 중 하나를 사용하지 않는 한 설명적인 것으로 간주됩니다.</p>
<p>부록에는 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-keywords-reference.html#appendix.query.method.predicate">정렬 및 대/소문자 구분 modifiers를 포함한 쿼리 method predicate 키워드</a>와 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-keywords-reference.html#appendix.query.method.subject">쿼리 메소드 subject 키워드의 전체 목록</a>이 포함되어 있습니다. 그러나 첫 번째 <code>By</code>는 실제 기준 predicate의 시작을 나타내는 구분 기호 역할을 합니다. 매우 기본적인 수준에서는 엔터티 속성(property)에 대한 조건을 정의하고 이를 <code>And</code> 및 <code>Or</code>로 연결할 수 있습니다.</p>
<p>메서드 구문 분석의 실제 결과는 쿼리를 생성하는 persistence store에 따라 달라집니다. 그러나 주의해야 할 몇 가지 일반적인 사항이 있습니다.</p>
<ul>
<li><p>표현식은 일반적으로 연결될 수 있는 연산자와 결합된 property traversal입니다. <code>AND</code> 및 <code>OR</code>를 사용하여 속성(property) 표현식을 결합할 수 있습니다. 속성 표현식에 대해 <code>Between</code>, <code>LessThan</code>, <code>GreaterThan</code> 및 <code>Like</code>와 같은 연산자도 지원됩니다. 지원되는 연산자는 datastore에 따라 다를 수 있으므로 참조 문서의 해당 부분을 참조하세요.</p>
</li>
<li><p>메서드 구문 분석기는 개별 속성(예: <code>findByLastnameIgnoreCase(…)</code>) 또는 대소문자 무시를 지원하는 유형의 모든 속성(일반적으로 <code>String</code> 인스턴스 — 예: <code>findByLastnameAndFirstnameAllIgnoreCase(…)</code>)에 대해 <code>IgnoreCase</code> 플래그 설정을 지원합니다. 대소문자 무시 지원 여부는 store별로 다를 수 있으므로, store별 쿼리 방법은 참고문서의 해당 부분을 참고하세요.</p>
</li>
<li><p>속성(property)을 참조하는 쿼리 메서드에 <code>OrderBy</code> 절을 추가하고 정렬 방향(<code>Asc</code> 또는 <code>Desc</code>)을 제공하여 정적 순서를 적용할 수 있습니다. 동적 정렬을 지원하는 쿼리 방법을 만들려면 &quot;<a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#">페이징, 큰 결과 순회, 정렬 및 제한</a>&quot;을 참조하세요.</p>
</li>
</ul>
<h1 id="property-expressions">Property Expressions</h1>
<p>속성 식은 앞의 예에 표시된 것처럼 관리 엔터티의 직접 속성만 참조할 수 있습니다. 쿼리 생성 시 구문 분석된 속성이 관리되는 도메인 클래스의 속성인지 이미 확인했습니다. 그러나 중첩된 속성을 탐색하여 제약 조건을 정의할 수도 있습니다. 다음 메서드 서명을 고려하세요.</p>
<pre><code class="language-java">List&lt;Person&gt; findByAddressZipCode(ZipCode zipCode);</code></pre>
<p><code>Person</code>이 <code>ZipCode</code>가 있는 <code>Address</code>를 가지고 있다고 가정합니다. 이 경우 메소드는 <code>x.address.zipCode</code> 속성 순회(property traversal)를 생성합니다. resolution 알고리즘은 전체 부분(<code>AddressZipCode</code>)을 속성으로 해석하는 것으로 시작하고 도메인 클래스에서 해당 이름(대문자화되지 않음)을 가진 속성을 확인합니다. 알고리즘이 성공하면 해당 속성을 사용합니다. 그렇지 않은 경우 알고리즘은 오른쪽의 카멜 케이스 부분에서 소스를 머리와 꼬리로 분할하고 해당 속성(예: <code>AddressZip</code> 및 <code>Code</code>)을 찾으려고 시도합니다. 알고리즘이 해당 헤드가 있는 속성을 찾으면 꼬리를 가져와서 위에서 설명한 대로 꼬리를 분할하여 계속해서 트리를 만듭니다. 첫 번째 분할이 일치하지 않으면 알고리즘은 분할 지점을 왼쪽(<code>Address</code>, <code>ZipCode</code>)으로 이동하고 계속합니다.</p>
<p>이는 대부분의 경우 작동하지만 알고리즘이 잘못된 속성을 선택할 수도 있습니다. <code>Person</code> 클래스에 <code>addressZip</code> 속성도 있다고 가정합니다. 알고리즘은 이미 첫 번째 분할 라운드에서 일치하고 잘못된 속성을 선택하여 실패합니다(<code>addressZip</code> type에는 <code>code</code> 속성이 없을 수 있으므로).</p>
<p>이 모호함을 해결하려면 메서드 이름 안에 <code>_</code>를 사용하여 순회 지점을 수동으로 정의할 수 있습니다. 따라서 메소드 이름은 다음과 같습니다.</p>
<pre><code class="language-java">List&lt;Person&gt; findByAddress_ZipCode(ZipCode zipCode);</code></pre>
<p>밑줄 문자를 예약 문자로 취급하므로 표준 Java 명명 규칙을 따르는 것이 좋습니다(즉, 속성 이름에 밑줄을 사용하지 않고 대신 카멜 표기법을 사용하는 것).</p>
<h1 id="repository-methods-returning-collections-or-iterables">Repository Methods Returning Collections or Iterables</h1>
<p>여러 결과를 반환하는 쿼리 메서드는 표준 Java <code>Iterable</code>, <code>List</code> 및 <code>Set</code>을 사용할 수 있습니다. 그 외에도 우리는 <code>Iterable</code>의 사용자 정의 확장인 Spring Data의 <code>Streamable</code> 반환과 <a href="https://www.vavr.io/">Vavr</a>에서 제공하는 컬렉션 유형을 지원합니다. 가능한 모든 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/query-return-types-reference.html#appendix.query.return.types">쿼리 메소드 반환 유형</a>을 설명하는 부록을 참조하세요.</p>
<h2 id="using-streamable-as-query-method-return-type">Using Streamable as Query Method Return Type</h2>
<p><code>Iterable</code> 또는 모든 컬렉션 type 대신 <code>Streamable</code>을 사용할 수 있습니다. 비병렬 <code>Stream</code>(<code>Iterable</code>에는 없음)에 액세스하는 편리한 방법과 요소에 대해 직접 <code>....filter(...)</code> 및 <code>....map(...)</code>을 제공하고 <code>Streamable</code>을 다른 요소에 연결하는 기능을 제공합니다.</p>
<p>Using Streamable to combine query method results</p>
<pre><code class="language-java">interface PersonRepository extends Repository&lt;Person, Long&gt; {
  Streamable&lt;Person&gt; findByFirstnameContaining(String firstname);
  Streamable&lt;Person&gt; findByLastnameContaining(String lastname);
}

Streamable&lt;Person&gt; result = repository.findByFirstnameContaining(&quot;av&quot;)
  .and(repository.findByLastnameContaining(&quot;ea&quot;));</code></pre>
<h2 id="returning-custom-streamable-wrapper-types">Returning Custom Streamable Wrapper Types</h2>
<p>컬렉션에 대한 전용 래퍼 유형을 제공하는 것은 여러 요소를 반환하는 쿼리 결과에 대한 API를 제공하기 위해 일반적으로 사용되는 패턴입니다. 일반적으로 이러한 유형은 컬렉션과 유사한 유형을 반환하는 저장소 메서드를 호출하고 래퍼 유형의 인스턴스를 수동으로 생성하여 사용됩니다. Spring Data를 사용하면 다음 기준을 충족하는 경우 이러한 래퍼 유형을 쿼리 메서드 반환 유형으로 사용할 수 있으므로 추가 단계를 피할 수 있습니다.</p>
<ol>
<li><p>type은 <code>Streamable</code>을 구현합니다.</p>
</li>
<li><p>이 type은 생성자 또는 <code>Streamable</code>을 인수로 사용하는 <code>of(…)</code> 또는 <code>valueOf(…)</code>라는 정적 팩터리 메서드를 노출합니다.</p>
</li>
</ol>
<p>다음 목록은 예를 보여줍니다.</p>
<pre><code class="language-java">class Product { //(1)                                         
  MonetaryAmount getPrice() { … }
}

@RequiredArgsConstructor(staticName = &quot;of&quot;)
class Products implements Streamable&lt;Product&gt; { //(2)         

  private final Streamable&lt;Product&gt; streamable;

  public MonetaryAmount getTotal() {  //(3)                   
    return streamable.stream()
      .map(Priced::getPrice)
      .reduce(Money.of(0), MonetaryAmount::add);
  }


  @Override
  public Iterator&lt;Product&gt; iterator() { //(4)                
    return streamable.iterator();
  }
}

interface ProductRepository implements Repository&lt;Product, Long&gt; {
  Products findAllByDescriptionContaining(String text);  //(5)
}</code></pre>
<p>(1) 제품 가격에 액세스하기 위해 API를 노출하는 <code>Product</code> 엔터티입니다.
(2) <code>Products.of(…)</code>(Lombok annotation으로 생성된 팩토리 메서드)를 사용하여 생성할 수 있는 <code>Streamable&lt;Product&gt;</code>에 대한 래퍼 type입니다. <code>Streamable&lt;Product&gt;</code>를 취하는 표준 생성자도 마찬가지입니다.
(3) 래퍼 type은 <code>Streamable&lt;Product&gt;</code>에서 새 값을 계산하는 추가 API를 노출합니다.
(4) <code>Streamable</code> 인터페이스를 구현하고 실제 결과에 위임합니다.
(5) 해당 래퍼 유형 <code>Products</code>은 쿼리 메서드 반환 유형으로 직접 사용할 수 있습니다. <code>Streamable&lt;Product&gt;</code>를 반환하고 리포지토리 클라이언트에서 쿼리 후에 수동으로 래핑할 필요가 없습니다.</p>
<h2 id="support-for-vavr-collections">Support for Vavr Collections</h2>
<p><a href="https://www.vavr.io/">Vavr</a>은 Java의 함수형 프로그래밍 개념을 수용하는 라이브러리입니다. 다음 표에 표시된 것처럼 쿼리 메서드 반환 유형으로 사용할 수 있는 사용자 지정 컬렉션 유형 집합이 함께 제공됩니다</p>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 33.3333%;">
<col style="width: 33.3333%;">
<col style="width: 33.3334%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Vavr 컬렉션 유형</th>
<th class="tableblock halign-left valign-top">사용된 Vavr 구현 유형</th>
<th class="tableblock halign-left valign-top">유효한 Java 소스 유형</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>io.vavr.collection.Seq</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>io.vavr.collection.List</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>java.util.Iterable</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>io.vavr.collection.Set</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>io.vavr.collection.LinkedHashSet</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>java.util.Iterable</code></p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>io.vavr.collection.Map</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>io.vavr.collection.LinkedHashMap</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>java.util.Map</code></p></td>
</tr>
</tbody>
</table>

<p>실제 쿼리 결과(세 번째 열)의 Java 유형에 따라 첫 번째 열의 유형(또는 그 하위 유형)을 쿼리 메소드 반환 유형으로 사용하고 두 번째 열의 유형을 구현 유형으로 가져올 수 있습니다. 또는 <code>Traversable</code>(Vavr <code>Iterable</code>과 동일)을 선언한 다음 실제 반환 값에서 구현 클래스를 파생시킬 수 있습니다. 즉, <code>java.util.List</code>는 Vavr <code>List</code> 또는 <code>Seq</code>로 바뀌고, <code>java.util.Set</code>은 Vavr <code>LinkedHashSet</code> <code>Set</code>이 되는 식입니다.</p>
<h1 id="streaming-query-results">Streaming Query Results</h1>
<p>Java 8 <code>Stream&lt;T&gt;</code>을 반환 유형으로 사용하여 쿼리 메서드의 결과를 증분적으로 처리할 수 있습니다. 쿼리 결과를 <code>Stream</code>으로 래핑하는 대신 다음 예제와 같이 데이터 저장소별 메서드를 사용하여 스트리밍을 수행합니다.</p>
<p>Java 8 <code>Stream&lt;T&gt;</code>을 사용하여 쿼리 결과 스트리밍</p>
<pre><code class="language-java">@Query(&quot;select u from User u&quot;)
Stream&lt;User&gt; findAllByCustomQueryAndStream();

Stream&lt;User&gt; readAllByFirstnameNotNull();

@Query(&quot;select u from User u&quot;)
Stream&lt;User&gt; streamAllPaged(Pageable pageable);</code></pre>
<blockquote>
<p><strong>[Note]</strong>
<code>Stream</code>은 잠재적으로 기본 데이터 저장소별 리소스를 래핑하므로 사용 후에는 닫혀야 합니다. 다음 예제와 같이 <code>close()</code> 메서드를 사용하거나 Java 7 <code>try-with-resources</code> 블록을 사용하여 <code>Stream</code>을 수동으로 닫을 수 있습니다.</p>
</blockquote>
<p><code>Stream&lt;T&gt;</code>로 작업하면 <code>try-with-resources</code> 블록이 발생합니다.</p>
<pre><code class="language-java">try (Stream&lt;User&gt; stream = repository.findAllByCustomQueryAndStream()) {
  stream.forEach(…);
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
현재 모든 Spring Data 모듈이 <code>Stream&lt;T&gt;</code>을 반환 유형으로 지원하는 것은 아닙니다.</p>
</blockquote>
<h1 id="asynchronous-query-results">Asynchronous Query Results</h1>
<p><a href="https://docs.spring.io/spring-framework/reference/6.1/integration/scheduling.html">Spring의 비동기 메소드 실행 기능</a>을 사용하여 저장소 쿼리를 비동기적으로 실행할 수 있습니다. 이는 Spring <code>TaskExecutor</code>에 제출된 작업에서 실제 쿼리가 발생하는 동안 메서드가 호출 즉시 반환됨을 의미합니다. 비동기 쿼리는 반응 쿼리와 다르므로 혼합되어서는 안 됩니다. 대응적 지원에 대한 자세한 내용은 매장별 설명서를 참조하세요. 다음 예에서는 다양한 비동기 쿼리를 보여줍니다.</p>
<pre><code class="language-java">@Async
Future&lt;User&gt; findByFirstname(String firstname); // (1)

@Async
CompletableFuture&lt;User&gt; findOneByFirstname(String firstname); // (2)</code></pre>
<p>(1) 반환 유형으로 <code>java.util.concurrent.Future</code>를 사용합니다. 
(2) 반환 유형으로 Java 8 <code>java.util.concurrent.CompletableFuture</code>를 사용합니다.</p>
<h1 id="paging-iterating-large-results-sorting--limiting">Paging, Iterating Large Results, Sorting &amp; Limiting</h1>
<p>쿼리의 매개변수를 처리하려면 이전 예제에서 이미 본 것처럼 메서드 매개변수를 정의합니다. 그 외에도 인프라는 <code>Pageable</code>, <code>Sort</code> 및 <code>Limit</code>과 같은 특정 type을 인식하여 쿼리에 페이지 매김, 정렬 및 제한을 동적으로 적용합니다. 다음 예에서는 이러한 기능을 보여줍니다.</p>
<p>쿼리 메서드에서 Pageable, Slice, Sort 및 Limit 사용</p>
<pre><code class="language-java">Page&lt;User&gt; findByLastname(String lastname, Pageable pageable);

Slice&lt;User&gt; findByLastname(String lastname, Pageable pageable);

List&lt;User&gt; findByLastname(String lastname, Sort sort);

List&lt;User&gt; findByLastname(String lastname, Sort sort, Limit limit);

List&lt;User&gt; findByLastname(String lastname, Pageable pageable);</code></pre>
<blockquote>
<p><strong>[Important]</strong>
<code>Sort</code>, <code>Pageable</code> 및 <code>Limit</code>을 사용하는 API는 <code>null</code>이 아닌 값이 메서드에 전달될 것으로 예상합니다. 정렬이나 페이지 매김을 적용하지 않으려면 <code>Sort.unsorted()</code>, <code>Pageable.unpaged()</code> 및 <code>Limit.unlimited()</code>를 사용하세요.</p>
</blockquote>
<p>첫 번째 방법을 사용하면 <code>org.springframework.data.domain.Pageable</code> 인스턴스를 쿼리 메서드에 전달하여 정적으로 정의된 쿼리에 페이징을 동적으로 추가할 수 있습니다. <code>Page</code>는 사용 가능한 총 요소 및 페이지 수를 알고 있습니다. 전체 숫자를 계산하기 위해 개수 쿼리를 트리거하는 인프라를 통해 이를 수행합니다. 비용이 많이 들 수 있으므로(사용된 저장소에 따라) 대신 <code>Slice</code>를 반환할 수 있습니다. <code>Slice</code>는 다음 <code>Slice</code>가 사용 가능한지 여부만 알고 있으며 이는 더 큰 결과 세트를 탐색할 때 충분할 수 있습니다.</p>
<p>정렬 옵션도 <code>Pageable</code> 인스턴스를 통해 처리됩니다. 정렬만 필요한 경우 <code>org.springframework.data.domain.Sort</code> 매개변수를 메서드에 추가하세요. 보시다시피 <code>List</code>를 반환하는 것도 가능합니다. 이 경우 실제 <code>Page</code> 인스턴스를 구축하는 데 필요한 추가 메타데이터가 생성되지 않습니다(즉, 필요했던 추가 개수 쿼리가 실행되지 않음을 의미). 오히려 지정된 엔터티 범위만 조회하도록 쿼리를 제한합니다.</p>
<blockquote>
<p><strong>[Note]</strong>
전체 쿼리에 대해 얻은 페이지 수를 확인하려면 추가 개수 쿼리를 실행해야 합니다. 기본적으로 이 쿼리는 실제로 트리거한 쿼리에서 파생됩니다.</p>
</blockquote>
<blockquote>
<p><strong>[Important]</strong>
특수 매개변수는 쿼리 메소드 내에서 한 번만 사용할 수 있습니다.
위에 설명된 일부 특수 매개변수는 상호 배타적입니다. 다음의 잘못된 매개변수 조합 목록을 고려하세요.</p>
</blockquote>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 33.3333%;">
<col style="width: 33.3333%;">
<col style="width: 33.3334%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">매개변수</th>
<th class="tableblock halign-left valign-top">예시</th>
<th class="tableblock halign-left valign-top">이유</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable</code> 및 <code>Sort</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findBy…​(Pageable page, Sort sort)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable</code>은 이미 <code>Sort</code>를 정의하고 있습니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable</code> 및 <code>Limit</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>findBy…​(Pageable page, Limit limit)</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable</code>은 이미 한도를 정의하고 있습니다.</p></td>
</tr>
</tbody>
</table>

<blockquote>
<p>결과를 제한하는 데 사용되는 <code>Top</code> 키워드는 <code>Pageable</code>과 함께 사용할 수 있는 반면 <code>Top</code>은 결과의 총 최대값을 정의하는 반면 Pageable 매개변수는 이 숫자를 줄일 수 있습니다.</p>
</blockquote>
<h2 id="which-method-is-appropriate">Which Method is Appropriate?</h2>
<p>Spring Data 추상화에서 제공하는 값은 아마도 아래 표에 설명된 가능한 쿼리 메서드 반환 유형으로 가장 잘 표시될 것입니다. 표에는 쿼리 메서드에서 반환할 수 있는 유형이 나와 있습니다.</p>
<p>Table 1. Consuming Large Query Results</p>
<table class="tableblock frame-all grid-all stretch">
<caption class="title">표 1. 대용량 쿼리 결과 소비</caption>
<colgroup>
<col style="width: 12.5%;">
<col style="width: 25%;">
<col style="width: 25%;">
<col style="width: 37.5%;">
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">방법</th>
<th class="tableblock halign-left valign-top">데이터 검색 양</th>
<th class="tableblock halign-left valign-top">쿼리 구조</th>
<th class="tableblock halign-left valign-top">제약 사항</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#repositories.collections-and-iterables"><code>List&lt;T&gt;</code></a></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">모든 결과.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">단일 쿼리.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">쿼리 결과가 모든 메모리를 소진할 수 있습니다. 모든 데이터를 검색하는 데 시간이 오래 걸릴 수 있습니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#repositories.collections-and-iterables.streamable"><code>Streamable&lt;T&gt;</code></a></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">모든 결과.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">단일 쿼리.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">쿼리 결과가 모든 메모리를 소진할 수 있습니다. 모든 데이터를 검색하는 데 시간이 오래 걸릴 수 있습니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><a href="#repositories.query-streaming"><code>Stream&lt;T&gt;</code></a></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">체크된 (하나씩 또는 일괄 처리로) <code>Stream</code> 소비에 따라 청크화됩니다.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">일반적으로 커서를 사용한 단일 쿼리.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">리소스 누수를 피하기 위해 사용 후 스트림을 닫아야 합니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Flux&lt;T&gt;</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">체크된 (하나씩 또는 일괄 처리로) <code>Flux</code> 소비에 따라 청크화됩니다.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">일반적으로 커서를 사용한 단일 쿼리.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">저장 모듈이 반응형 인프라를 제공해야 합니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Slice&lt;T&gt;</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable.getOffset()</code>에서 시작하는 데이터 검색 양은 <code>Pageable.getPageSize() + 1</code>입니다.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable.getOffset()</code>에서 시작하는 데이터를 검색하는 일대다 쿼리를 적용하여 데이터를 검색합니다.</p></td>
<td class="tableblock halign-left valign-top"><div class="content"><div class="paragraph">
<p><code>Slice</code>는 다음 <code>Slice</code>로 이동할 수 있습니다.</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>Slice</code>는 더 많은 데이터를 검색할 수 있는지에 대한 세부 정보를 제공합니다.</p>
</li>
<li>
<p>오프셋 기반 쿼리는 오프셋이 너무 큰 경우 효율이 떨어집니다. 왜냐하면 데이터베이스는 여전히 전체 결과를 실체화해야 하기 때문입니다.</p>
</li>
<li>
<p><code>Window</code>는 더 많은 데이터를 검색할 수 있는지에 대한 세부 정보를 제공합니다.</p>
</li>
<li>
<p>오프셋 기반 쿼리는 오프셋이 너무 큰 경우 효율이 떨어집니다. 왜냐하면 데이터베이스는 여전히 전체 결과를 실체화해야 하기 때문입니다.</p>
</li>
</ul>
</div></div></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Page&lt;T&gt;</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable.getOffset()</code>에서 시작하는 데이터 검색 양은 <code>Pageable.getPageSize()</code>입니다.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>Pageable.getOffset()</code>에서 시작하는 일대다 쿼리를 적용하여 데이터를 검색합니다. 추가로, 총 요소 수를 결정하기 위해 <code>COUNT(…)</code> 쿼리가 필요할 수 있습니다.</p></td>
<td class="tableblock halign-left valign-top"><div class="content"><div class="paragraph">
<p>자주 <code>COUNT(…)</code> 쿼리가 필요하며 이는 비용이 많이 듭니다.</p>
</div>
<div class="ulist">
<ul>
<li>
<p>오프셋 기반 쿼리는 오프셋이 너무 큰 경우 효율이 떨어집니다. 왜냐하면 데이터베이스는 여전히 전체 결과를 실체화해야 하기 때문입니다.</p>
</li>
</ul>
</div></div></td>
</tr>
</tbody>
</table>

<h2 id="paging-and-sorting">Paging and Sorting</h2>
<p>속성 이름을 사용하여 간단한 정렬 표현식을 정의할 수 있습니다. 표현식을 연결하여 여러 기준을 하나의 표현식으로 수집할 수 있습니다.</p>
<p>Defining sort expressions</p>
<pre><code class="language-java">Sort sort = Sort.by(&quot;firstname&quot;).ascending()
  .and(Sort.by(&quot;lastname&quot;).descending());</code></pre>
<p>보다 형식에 안전한 정렬 식을 정의하려면 정렬 식을 정의할 형식부터 시작하고 메서드 참조를 사용하여 정렬할 속성을 정의하세요.</p>
<p>유형이 안전한 API를 사용하여 정렬 표현식 정의</p>
<pre><code class="language-java">TypedSort&lt;Person&gt; person = Sort.sort(Person.class);

Sort sort = person.by(Person::getFirstname).ascending()
  .and(person.by(Person::getLastname).descending());</code></pre>
<blockquote>
<p><strong>[Note]</strong>
<code>TypedSort.by(…)</code>는 (일반적으로) CGlib를 사용하여 런타임 프록시를 사용합니다. 이는 Graal VM Native와 같은 도구를 사용할 때 기본 이미지 컴파일을 방해할 수 있습니다.</p>
</blockquote>
<p>저장소 구현이 Querydsl을 지원하는 경우 생성된 메타모델 유형을 사용하여 정렬 표현식을 정의할 수도 있습니다.</p>
<p>Defining sort expressions by using the Querydsl API</p>
<pre><code class="language-java">QSort sort = QSort.by(QPerson.firstname.asc())
  .and(QSort.by(QPerson.lastname.desc()));</code></pre>
<h2 id="limiting-query-results">Limiting Query Results</h2>
<p>페이징 외에도 전용 <code>Limit</code> 매개변수를 사용하여 결과 크기를 제한할 수 있습니다. 또한 <code>First</code> 또는 <code>Top</code> 키워드를 사용하여 쿼리 메서드의 결과를 제한할 수도 있습니다. 이 키워드는 서로 바꿔서 사용할 수 있지만 <code>Limit</code> 매개 변수와 혼합할 수는 없습니다. <code>Top</code> 또는 <code>First</code>에 선택적 숫자 값을 추가하여 반환할 최대 결과 크기를 지정할 수 있습니다. 숫자를 생략하면 결과 크기가 1로 가정됩니다. 다음 예에서는 쿼리 크기를 제한하는 방법을 보여줍니다.</p>
<p>Top 및 First를 사용하여 쿼리 결과 크기 제한</p>
<pre><code class="language-java">List&lt;User&gt; findByLastname(Limit limit);

User findFirstByOrderByLastnameAsc();

User findTopByOrderByAgeDesc();

Page&lt;User&gt; queryFirst10ByLastname(String lastname, Pageable pageable);

Slice&lt;User&gt; findTop3ByLastname(String lastname, Pageable pageable);

List&lt;User&gt; findFirst10ByLastname(String lastname, Sort sort);

List&lt;User&gt; findTop10ByLastname(String lastname, Pageable pageable);</code></pre>
<p>제한 표현식은 고유 쿼리를 지원하는 데이터 저장소에 대해 <code>Distinct</code> 키워드도 지원합니다. 또한 결과 집합을 하나의 인스턴스로 제한하는 쿼리의 경우 <code>Optional</code> 키워드를 사용하여 결과를 래핑하는 것이 지원됩니다.</p>
<p>제한된 쿼리 페이지네이션(및 사용 가능한 페이지 수 계산)에 페이지네이션 또는 슬라이싱을 적용하는 경우 제한된 결과 내에서 적용됩니다.</p>
<blockquote>
<p><strong>[Note]</strong>
<code>Sort</code> 매개변수를 사용하여 동적 정렬과 함께 결과를 제한하면 &#39;K&#39; 가장 작은 요소뿐만 아니라 &#39;K&#39; 가장 큰 요소에 대한 쿼리 방법을 표현할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Persisting Entities]]></title>
            <link>https://velog.io/@dev_hammy/Persisting-Entities</link>
            <guid>https://velog.io/@dev_hammy/Persisting-Entities</guid>
            <pubDate>Fri, 19 Apr 2024 17:15:14 GMT</pubDate>
            <description><![CDATA[<p>이 섹션에서는 Spring Data JPA를 사용하여 엔터티를 유지(저장)하는 방법을 설명합니다.</p>
<h1 id="saving-entities">Saving Entities</h1>
<p><code>CrudRepository.save(…)</code> 메소드를 사용하여 엔터티 저장을 수행할 수 있습니다. 기본 JPA <code>EntityManager</code>를 사용하여 지정된 엔터티를 유지하거나 병합합니다. 엔터티가 아직 지속되지 않은 경우 Spring Data JPA는 <code>entityManager.persist(…)</code> 메서드를 호출하여 엔터티를 저장합니다. 그렇지 않으면 <code>entityManager.merge(…)</code> 메서드를 호출합니다.</p>
<h2 id="entity-state-detection-strategies">Entity State-detection Strategies</h2>
<p>Spring Data JPA는 엔터티가 새로운지 여부를 감지하기 위해 다음 전략을 제공합니다.</p>
<ol>
<li><p>Version-Property 및 Id-Property 검사(기본값): 기본적으로 Spring Data JPA는 non-primitive type의 버전 속성이 있는지 먼저 검사합니다. 있는 경우 해당 속성의 값이 <code>null</code>이면 엔터티는 새로운 것으로 간주됩니다. 이러한 버전 속성이 없으면 Spring Data JPA는 지정된 엔터티의 식별자 속성을 검사합니다. 식별자 속성이 <code>null</code>이면 엔터티는 새로운 엔터티로 간주됩니다. 그렇지 않으면 새로운 것이 아닌 것으로 간주됩니다.</p>
</li>
<li><p><code>Persistable</code> 구현: 엔터티가 <code>Persistable</code>을 구현하는 경우 Spring Data JPA는 새로운 감지를 엔터티의 <code>isNew(…)</code> 메서드에 위임합니다. 자세한 내용은 <a href="https://docs.spring.io/spring-data/data-commons/docs/current/api/index.html?org/springframework/data/domain/Persistable.html">JavaDoc</a>을 참조하세요.</p>
</li>
<li><p><code>EntityInformation</code> 구현: <code>JpaRepositoryFactory</code>의 하위 클래스를 생성하고 이에 따라 <code>getEntityInformation(…)</code> 메서드를 재정의하여 <code>SimpleJpaRepository</code> 구현에 사용되는 <code>EntityInformation</code> 추상화를 사용자 정의할 수 있습니다. 그런 다음 <code>JpaRepositoryFactory</code>의 사용자 정의 구현을 Spring Bean으로 등록해야 합니다. 이는 거의 필요하지 않습니다. 자세한 내용은 <a href="https://docs.spring.io/spring-data/data-jpa/docs/current/api/index.html?org/springframework/data/jpa/repository/support/JpaRepositoryFactory.html">JavaDoc</a>을 참조하세요.</p>
</li>
</ol>
<p>옵션 1은 수동으로 할당된 식별자를 사용하고 버전 속성이 없는 엔터티에 대한 옵션이 아닙니다. 식별자는 항상 <code>null</code>이 아니기 때문입니다. 해당 시나리오의 일반적인 패턴은 기본적으로 새 인스턴스를 표시하는 transient 플래그가 있는 공통 기본 클래스를 사용하고 JPA lifecycle 콜백을 사용하여 영속성 작업에서 해당 플래그를 뒤집는 것입니다.</p>
<p>Example 1. A base class for entities with manually assigned identifiers</p>
<pre><code class="language-java">@MappedSuperclass
public abstract class AbstractEntity&lt;ID&gt; implements Persistable&lt;ID&gt; {

  @Transient
  private boolean isNew = true; // (1)

  @Override
  public boolean isNew() {
    return isNew; // (2) 
  }

  @PrePersist // (3)
  @PostLoad
  void markNotNew() {
    this.isNew = false;
  }

  // More code…
}</code></pre>
<p>(1) 새로운 상태를 유지하기 위한 플래그를 선언합니다. 데이터베이스에 유지되지 않도록 일시적입니다.
(2) Spring Data 저장소가 <code>EntityManager.persist()</code> 또는 …<code>.merge()</code>를 호출할지 여부를 알 수 있도록 <code>Persistable.isNew()</code> 구현에서 플래그를 반환합니다.
(3) <code>save(...)</code>에 대한 저장소 호출 또는 지속성 공급자에 의한 인스턴스 생성 후에 플래그가 기존 엔터티를 나타내도록 전환되도록 JPA 엔터티 콜백을 사용하여 메서드를 선언합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Configuration]]></title>
            <link>https://velog.io/@dev_hammy/Configuration</link>
            <guid>https://velog.io/@dev_hammy/Configuration</guid>
            <pubDate>Fri, 19 Apr 2024 16:47:09 GMT</pubDate>
            <description><![CDATA[<p>이 섹션에서는 다음 중 하나를 통해 Spring Data JPA를 구성하는 방법을 설명합니다. </p>
<ul>
<li><a href="https://docs.spring.io/spring-data/jpa/reference/repositories/create-instances.html#jpa.java-config">&quot;annotation 기반 구성&quot;</a>(Java 구성) </li>
<li><a href="https://docs.spring.io/spring-data/jpa/reference/repositories/create-instances.html#repositories.create-instances.xml">“Spring 네임스페이스”</a>(XML 구성)</li>
</ul>
<h1 id="annotation-based-configuration">Annotation-based Configuration</h1>
<p>Spring Data JPA 리포지토리 지원은 다음 예제와 같이 JavaConfig와 사용자 정의 XML 네임스페이스를 통해 활성화될 수 있습니다.</p>
<p>Example 1. Spring Data JPA repositories using JavaConfig</p>
<pre><code class="language-java">@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
class ApplicationConfig {

  @Bean
  public DataSource dataSource() {

    EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
    return builder.setType(EmbeddedDatabaseType.HSQL).build();
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() {

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setGenerateDdl(true);

    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setPackagesToScan(&quot;com.acme.domain&quot;);
    factory.setDataSource(dataSource());
    return factory;
  }

  @Bean
  public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {

    JpaTransactionManager txManager = new JpaTransactionManager();
    txManager.setEntityManagerFactory(entityManagerFactory);
    return txManager;
  }
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
<code>EntityManagerFactory</code>를 생성하는 것 외에도 예외 변환 메커니즘에도 참여하므로 <code>EntityManagerFactory</code>가 아닌 <code>LocalContainerEntityManagerFactoryBean</code>을 직접 생성해야 합니다.</p>
</blockquote>
<p>앞의 구성 클래스는 <code>spring-jdbc</code>의 <code>EmbeddedDatabaseBuilder</code> API를 사용하여 임베디드 HSQL 데이터베이스를 설정합니다. 그런 다음 Spring Data는 <code>EntityManagerFactory</code>를 설정하고 Hibernate를 샘플 persistence provider로 사용합니다. 여기에 선언된 마지막 인프라 구성 요소는 <code>JpaTransactionManager</code>입니다. 마지막으로 이 예제에서는 본질적으로 XML 네임스페이스와 동일한 속성을 전달하는 <code>@EnableJpaRepositories</code> annotation을 사용하여 Spring Data JPA 저장소를 활성화합니다. base package가 구성되어 있지 않으면 configuration 클래스가 있는 패키지를 사용합니다.</p>
<h1 id="spring-namespace">Spring Namespace</h1>
<p>Spring Data의 JPA 모듈에는 저장소 Bean 정의를 허용하는 사용자 정의 네임스페이스가 포함되어 있습니다. 또한 JPA에 특별한 특정 기능과 요소 속성도 포함되어 있습니다. 일반적으로 JPA 저장소는 다음 예제와 같이 <code>repositories</code> 요소를 사용하여 설정할 수 있습니다.</p>
<p>Example 2. Setting up JPA repositories by using the namespace</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
  xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
  xmlns:jpa=&quot;http://www.springframework.org/schema/data/jpa&quot;
  xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/data/jpa
    https://www.springframework.org/schema/data/jpa/spring-jpa.xsd&quot;&gt;

  &lt;jpa:repositories base-package=&quot;com.acme.repositories&quot; /&gt;

&lt;/beans&gt;</code></pre>
<blockquote>
<p><strong>[Tip]</strong>
JavaConfig와 XML 중 어느 것이 더 낫습니까? XML은 오래 전에 Spring이 구성된 방식입니다. 오늘날 빠르게 성장하는 Java, 레코드 유형, 주석 등의 시대에 새로운 프로젝트는 일반적으로 가능한 한 많은 순수 Java를 사용합니다. XML 지원을 즉시 제거할 계획은 없지만 최신 기능 중 일부는 XML을 통해 사용하지 못할 수도 있습니다.</p>
</blockquote>
<p><code>repositories</code> 요소를 사용하면 <code>@Repository</code>로 annotation이 달린 모든 빈에 대한 persistence 예외 변환이 활성화되어 JPA persistence provider가 던진 예외가 Spring의 <code>DataAccessException</code> 계층으로 변환될 수 있습니다.</p>
<h2 id="custom-namespace-attributes">Custom Namespace Attributes</h2>
<p><code>repositories</code> 요소의 기본 속성 외에도 JPA 네임스페이스는 저장소 설정을 보다 세부적으로 제어할 수 있는 추가 속성을 제공합니다.</p>
<p>Table 1. Custom JPA-specific attributes of the repositories element</p>
<table class="tableblock frame-all grid-all fit-content">
<caption class="title">표 1. <code>repositories</code> 요소의 사용자 정의 JPA 특정 속성</caption>
<colgroup>
<col>
<col>
</colgroup>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>entity-manager-factory-ref</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>repositories</code> 요소에서 감지된 리포지토리에 사용할 <code>EntityManagerFactory</code>를 명시적으로 연결합니다. 일반적으로 응용 프로그램 내에서 여러 개의 <code>EntityManagerFactory</code> 빈을 사용하는 경우에 사용됩니다. 구성되지 않은 경우 Spring Data는 자동으로 <code>ApplicationContext</code>에서 이름이 <code>entityManagerFactory</code>인 <code>EntityManagerFactory</code> 빈을 찾습니다.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>transaction-manager-ref</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><code>repositories</code> 요소에서 감지된 리포지토리에 사용할 <code>PlatformTransactionManager</code>를 명시적으로 연결합니다. 일반적으로 여러 개의 트랜잭션 관리자 또는 <code>EntityManagerFactory</code> 빈이 구성된 경우에만 필요합니다. 현재 <code>ApplicationContext</code> 내에서 정의된 단일 지정된 <code>PlatformTransactionManager</code>로 기본 설정됩니다.</p></td>
</tr>
</tbody>
</table>

<blockquote>
<p><strong>[Note]</strong>
Spring Data JPA에서는 명시적인 <code>transaction-manager-ref</code>가 정의되지 않은 경우 <code>transactionManager</code>라는 <code>PlatformTransactionManager</code> 빈이 있어야 합니다.</p>
</blockquote>
<h1 id="bootstrap-mode">Bootstrap Mode</h1>
<p>기본적으로 Spring Data JPA 리포지토리는 기본 Spring Bean입니다. 싱글톤 범위이며 열심히 초기화됩니다. 시작하는 동안 검증 및 메타데이터 분석 목적으로 이미 JPA <code>EntityManager</code>와 상호 작용합니다. Spring Framework는 백그라운드 스레드에서 JPA <code>EntityManagerFactory</code>의 초기화를 지원합니다. 그 이유는 해당 프로세스가 일반적으로 Spring 애플리케이션에서 상당한 시작 시간을 차지하기 때문입니다. 백그라운드 초기화를 효과적으로 사용하려면 JPA 저장소가 가능한 한 늦게 초기화되도록 해야 합니다.</p>
<p>Spring Data JPA 2.1부터 이제 다음 값을 사용하는 <code>BootstrapMode</code>(<code>@EnableJpaRepositories</code> annotation 또는 XML 네임스페이스를 통해)를 구성할 수 있습니다.</p>
<ul>
<li><p><code>DEFAULT</code>(기본값)  -  <code>@Lazy</code>로 명시적으로 주석을 달지 않는 한 리포지토리는 즉시 인스턴스화됩니다. 지연화는 리포지토리 빈의 초기화가 필요한 클라이언트 빈에 리포지토리 인스턴스가 필요하지 않은 경우에만 효과가 있습니다.</p>
</li>
<li><p><code>LAZY</code>  -  모든 저장소 Bean을 암시적으로 지연 선언하고 지연 초기화 프록시가 생성되어 클라이언트 Bean에 삽입되도록 합니다. 즉, 클라이언트 Bean이 초기화 중에 저장소를 사용하지 않고 단순히 필드에 인스턴스를 저장하는 경우 저장소가 인스턴스화되지 않습니다. 리포지토리 인스턴스는 리포지토리와 처음 상호 작용할 때 초기화되고 확인됩니다.</p>
</li>
<li><p><code>DEFERRED</code> — 기본적으로 <code>LAZY</code>와 동일한 작업 모드이지만 애플리케이션이 완전히 시작되기 전에 리포지토리가 확인되도록 <code>ContextRefreshedEvent</code>에 대한 응답으로 리포지토리 초기화를 트리거합니다.</p>
</li>
</ul>
<h2 id="recommendations">Recommendations</h2>
<p>기본 부트스트랩 모드로 비동기 JPA 부트스트랩 스틱을 사용하지 않는 경우.</p>
<p>JPA를 비동기적으로 부트스트랩하는 경우 <code>DEFERRED</code>는 합리적인 기본값입니다. 이는 Spring Data JPA 부트스트랩이 <code>EntityManagerFactory</code> 설정 자체가 다른 모든 애플리케이션 component를 초기화하는 것보다 오래 걸리는 경우에만 <code>EntityManagerFactory</code> 설정을 기다리도록 하기 때문입니다. 그럼에도 불구하고 애플리케이션이 신호를 보내기 전에 리포지토리가 적절하게 초기화되고 검증되었는지 확인합니다.</p>
<p><code>LAZY</code>는 시나리오 및 로컬 개발 테스트에 적합한 선택입니다. 리포지토리가 제대로 부트스트랩할 수 있다고 확신하거나 애플리케이션의 다른 부분을 테스트하는 경우 모든 리포지토리에 대해 확인을 실행하면 시작 시간이 불필요하게 늘어날 수 있습니다. 단일 리포지토리를 초기화해야 할 수 있는 애플리케이션 부분에만 액세스하는 로컬 개발에도 동일하게 적용됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Defining Repository Interfaces]]></title>
            <link>https://velog.io/@dev_hammy/Defining-Repository-Interfaces</link>
            <guid>https://velog.io/@dev_hammy/Defining-Repository-Interfaces</guid>
            <pubDate>Fri, 19 Apr 2024 15:36:05 GMT</pubDate>
            <description><![CDATA[<p>저장소 인터페이스를 정의하려면 먼저 도메인 클래스별 저장소 인터페이스를 정의해야 합니다. 인터페이스는 <code>Repository</code>를 확장해야 하며 도메인 클래스 및 ID 유형으로 입력되어야 합니다. 해당 도메인 유형에 대한 CRUD 메소드를 노출하려면 <code>Repository</code> 대신 <code>CrudRepository</code> 또는 그 변형 중 하나를 확장할 수 있습니다.</p>
<h1 id="fine-tuning-repository-definition">Fine-tuning Repository Definition</h1>
<p>저장소 인터페이스를 시작하는 방법에는 몇 가지 변형이 있습니다.</p>
<p>일반적인 접근 방식은 CRUD 기능에 대한 메서드를 제공하는 <code>CrudRepository</code>를 확장하는 것입니다. CRUD는 생성(Create), 읽기(Read), 업데이트(Update), 삭제(Delete)를 의미합니다. 버전 3.0에서는 <code>CrudRepository</code>와 매우 유사한 <code>ListCrudRepository</code>도 도입했지만 여러 엔터티를 반환하는 메서드의 경우 사용하기 더 쉬운 <code>Iterable</code> 대신 <code>List</code>를 반환합니다.</p>
<p>reactive store를 사용하는 경우 사용 중인 반응 프레임워크에 따라 <code>ReactiveCrudRepository</code> 또는 <code>RxJava3CrudRepository</code>를 선택할 수 있습니다.</p>
<p>Kotlin을 사용하는 경우 Kotlin의 코루틴을 활용하는 <code>CoroutineCrudRepository</code>를 선택할 수 있습니다.</p>
<p>추가로 <code>Sort</code> 추상화를 지정하거나 첫 번째 경우 <code>Pageable</code> 추상화를 지정할 수 있는 메서드가 필요한 경우 <code>PagingAndSortingRepository</code>, <code>ReactiveSortingRepository</code>, <code>RxJava3SortingRepository</code> 또는 <code>CoroutineSortingRepository</code>를 확장할 수 있습니다. 다양한 정렬 저장소는 3.0 이전의 Spring Data 버전에서처럼 더 이상 해당 CRUD 저장소를 확장하지 않습니다. 따라서 두 인터페이스의 기능을 모두 사용하려면 두 인터페이스를 모두 확장해야 합니다.</p>
<p>Spring Data 인터페이스를 확장하지 않으려면 <code>@RepositoryDefinition</code>을 사용하여 저장소 인터페이스에 주석을 달 수도 있습니다. CRUD 저장소 인터페이스 중 하나를 확장하면 엔터티를 조작하기 위한 전체 메서드 세트가 노출됩니다. 노출되는 메서드를 선택적으로 선택하려면 CRUD 저장소에서 도메인 저장소로 노출하려는 메서드를 복사하세요. 이때 메소드의 반환 유형을 변경할 수 있습니다. Spring Data는 가능하다면 반환 유형을 존중합니다. 예를 들어 여러 엔터티를 반환하는 메서드의 경우 <code>Iterable&lt;T&gt;</code>, <code>List&lt;T&gt;</code>, <code>Collection&lt;T&gt;</code> 또는 VAVR 목록을 선택할 수 있습니다.</p>
<p>애플리케이션의 많은 저장소에 동일한 메소드 세트가 있어야 하는 경우 상속할 자체 기본 인터페이스를 정의할 수 있습니다. 이러한 인터페이스에는 <code>@NoRepositoryBean</code>으로 annotation을 달아야 합니다. 이렇게 하면 Spring Data가 인스턴스를 직접 생성하려고 시도하고 해당 저장소에 대한 엔터티를 결정할 수 없기 때문에 실패하는 것을 방지할 수 있습니다. 왜냐하면 여전히 generic type 변수가 포함되어 있기 때문입니다.</p>
<p>다음 예에서는 CRUD 메서드(이 경우 <code>findById</code> 및 <code>save</code>)를 선택적으로 노출하는 방법을 보여줍니다.</p>
<p>Selectively exposing CRUD methods</p>
<pre><code class="language-java">@NoRepositoryBean
interface MyBaseRepository&lt;T, ID&gt; extends Repository&lt;T, ID&gt; {

  Optional&lt;T&gt; findById(ID id);

  &lt;S extends T&gt; S save(S entity);
}

interface UserRepository extends MyBaseRepository&lt;User, Long&gt; {
  User findByEmailAddress(EmailAddress emailAddress);
}</code></pre>
<p>이전 예에서는 모든 도메인 저장소에 대한 공통 기본 인터페이스를 정의하고 <code>findById(...)</code> 및 <code>save(...)</code>를 노출했습니다. 이러한 메소드는 Spring Data에서 제공하는 선택한 저장소의 기본 저장소 구현으로 라우팅됩니다( 예를 들어 JPA를 사용하는 경우 구현은 <code>SimpleJpaRepository</code>입니다. 이는 <code>CrudRepository</code>의 메서드 서명과 일치하기 때문입니다. 따라서 <code>UserRepository</code>는 이제 사용자를 저장하고, ID로 개별 사용자를 찾고, 이메일 주소로 <code>Users</code>를 찾는 쿼리를 실행할 수 있습니다.</p>
<blockquote>
<p><strong>[Note]</strong>
중간 저장소 인터페이스에는 <code>@NoRepositoryBean</code>이라는 annotation이 붙습니다. Spring Data가 런타임에 인스턴스를 생성하지 말아야 하는 모든 저장소 인터페이스에 해당 annotation을 추가했는지 확인하세요.</p>
</blockquote>
<h1 id="using-repositories-with-multiple-spring-data-modules">Using Repositories with Multiple Spring Data Modules</h1>
<p>애플리케이션에서 고유한 Spring Data 모듈을 사용하면 정의된 범위의 모든 저장소 인터페이스가 Spring Data 모듈에 바인딩되므로 작업이 간단해집니다. 때때로 애플리케이션에서는 둘 이상의 Spring Data 모듈을 사용해야 합니다. 이러한 경우 저장소 정의는 지속성 기술을 구별해야 합니다. 클래스 경로에서 여러 저장소 팩토리를 감지하면 Spring Data는 엄격한 저장소 구성 모드로 들어갑니다. 엄격한 구성은 저장소 또는 도메인 클래스에 대한 세부 정보를 사용하여 저장소 정의에 대한 Spring Data 모듈 바인딩을 결정합니다.</p>
<ol>
<li><p>저장소 정의가<a href="https://docs.spring.io/spring-data/jpa/reference/repositories/definition.html#repositories.multiple-modules.types"> 모듈별 저장소를 확장하는 경우</a> 이는 특정 Spring Data 모듈에 대한 유효한 후보입니다.</p>
</li>
<li><p>도메인 클래스에 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/definition.html#repositories.multiple-modules.annotations">모듈별 type annotation이 달린 경우</a> 이는 특정 Spring Data 모듈에 대한 유효한 후보입니다. Spring Data 모듈은 제3자 annotation(예: JPA의 <code>@Entity</code>)을 허용하거나 자체 annotation(예: Spring Data MongoDB 및 Spring Data Elasticsearch의 <code>@Document</code>)을 제공합니다.</p>
</li>
</ol>
<p>다음 예에서는 모듈별 인터페이스(이 경우 JPA)를 사용하는 저장소를 보여줍니다.</p>
<p>Example 1. Repository definitions using module-specific interfaces</p>
<pre><code class="language-java">interface MyRepository extends JpaRepository&lt;User, Long&gt; { }

@NoRepositoryBean
interface MyBaseRepository&lt;T, ID&gt; extends JpaRepository&lt;T, ID&gt; { … }

interface UserRepository extends MyBaseRepository&lt;User, Long&gt; { … }</code></pre>
<p><code>MyRepository</code> 및 <code>UserRepository</code>는 type 계층 구조에서 <code>JpaRepository</code>를 확장합니다. 이는 Spring Data JPA 모듈의 유효한 후보입니다.</p>
<p>다음 예에서는 generic 인터페이스를 사용하는 저장소를 보여줍니다.</p>
<p>Example 2. Repository definitions using generic interfaces</p>
<pre><code class="language-java">interface AmbiguousRepository extends Repository&lt;User, Long&gt; { … }

@NoRepositoryBean
interface MyBaseRepository&lt;T, ID&gt; extends CrudRepository&lt;T, ID&gt; { … }

interface AmbiguousUserRepository extends MyBaseRepository&lt;User, Long&gt; { … }</code></pre>
<p><code>AmbiguousRepository</code> 및 <code>AmbiguousUserRepository</code>는 유형 계층 구조에서 <code>Repository</code> 및 <code>CrudRepository</code>만 확장합니다. 고유한 Spring Data 모듈을 사용할 때는 괜찮지만 여러 모듈이 이러한 저장소를 바인딩해야 하는 특정 Spring Data를 구별할 수 없습니다.</p>
<p>다음 예에서는 annotation이 포함된 도메인 클래스를 사용하는 저장소를 보여줍니다.</p>
<p>Example 3. Repository definitions using domain classes with annotations</p>
<pre><code class="language-java">interface PersonRepository extends Repository&lt;Person, Long&gt; { … }

@Entity
class Person { … }

interface UserRepository extends Repository&lt;User, Long&gt; { … }

@Document
class User { … }</code></pre>
<p><code>PersonRepository</code>는 JPA <code>@Entity</code> annotation이 달린 <code>Person</code>을 참조하므로 이 저장소는 분명히 Spring Data JPA에 속합니다. <code>UserRepository</code>는 Spring Data MongoDB의 <code>@Document</code> annotation이 달린 <code>User</code>를 참조합니다.</p>
<p>다음 나쁜 예는 annotation이 혼합된 도메인 클래스를 사용하는 저장소를 보여줍니다.</p>
<p>Example 4. Repository definitions using domain classes with mixed annotations</p>
<pre><code class="language-java">interface JpaPersonRepository extends Repository&lt;Person, Long&gt; { … }

interface MongoDBPersonRepository extends Repository&lt;Person, Long&gt; { … }

@Entity
@Document
class Person { … }</code></pre>
<p>이 예에서는 JPA 및 Spring Data MongoDB 주석을 모두 사용하는 도메인 클래스를 보여줍니다. <code>JpaPersonRepository</code>와 <code>MongoDBPersonRepository</code>라는 두 개의 저장소를 정의합니다. 하나는 JPA용이고 다른 하나는 MongoDB용입니다. Spring Data는 더 이상 리포지토리를 구분할 수 없으므로 정의되지 않은 동작이 발생합니다.</p>
<p>특정 Spring Data 모듈에 대한 저장소 후보를 식별하기 위한 엄격한 저장소 구성에 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/definition.html#repositories.multiple-modules.types">저장소 type 세부사항</a> 및 <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/definition.html#repositories.multiple-modules.annotations">구별되는 도메인 클래스 annotation</a>이 사용됩니다. 동일한 도메인 유형에 여러 영속성 기술별 annotation을 사용하는 것이 가능하며 여러 영속성 기술에서 도메인 type을 재사용할 수 있습니다. 그러나 Spring Data는 더 이상 저장소를 바인딩할 고유 모듈을 결정할 수 없습니다.</p>
<p>저장소를 구별하는 마지막 방법은 저장소 기본 패키지의 범위를 지정하는 것입니다. base package는 저장소 인터페이스 정의를 검색하기 위한 시작점을 정의합니다. 이는 저장소 정의가 적절한 패키지에 있다는 것을 의미합니다. 기본적으로 anootation 기반 구성은 configuration 클래스 패키지를 사용합니다. <a href="https://docs.spring.io/spring-data/jpa/reference/repositories/create-instances.html#repositories.create-instances.xml">XML 기반 구성의 base package</a>는 필수입니다.</p>
<p>다음 예에서는 base package의 annotation 기반 구성을 보여줍니다.</p>
<p>Annotation-driven configuration of base packages</p>
<pre><code class="language-java">@EnableJpaRepositories(basePackages = &quot;com.acme.repositories.jpa&quot;)
@EnableMongoRepositories(basePackages = &quot;com.acme.repositories.mongo&quot;)
class Configuration { … }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Core concepts]]></title>
            <link>https://velog.io/@dev_hammy/Core-concepts</link>
            <guid>https://velog.io/@dev_hammy/Core-concepts</guid>
            <pubDate>Fri, 19 Apr 2024 14:47:15 GMT</pubDate>
            <description><![CDATA[<p>Spring Data 저장소 추상화의 중앙 인터페이스는 <code>Repository</code>입니다. 관리할 도메인 클래스와 도메인 클래스의 식별자 유형을 유형 인수로 사용합니다. 이 인터페이스는 주로 작업할 유형을 캡처하고 이 인터페이스를 확장하는 인터페이스를 검색하는 데 도움이 되는 마커 인터페이스 역할을 합니다. <a href="https://docs.spring.io/spring-data/commons/docs/3.2.5/api//org/springframework/data/repository/CrudRepository.html"><code>CrudRepository</code></a> 및 <a href="https://docs.spring.io/spring-data/commons/docs/3.2.5/api//org/springframework/data/repository/ListCrudRepository.html"><code>ListCrudRepository</code></a> 인터페이스는 관리되는 엔터티 클래스에 대한 정교한 CRUD 기능을 제공합니다.</p>
<p>CrudRepository Interface</p>
<pre><code class="language-java">public interface CrudRepository&lt;T, ID&gt; extends Repository&lt;T, ID&gt; {

  &lt;S extends T&gt; S save(S entity); // (1)

  Optional&lt;T&gt; findById(ID primaryKey); // (2)

  Iterable&lt;T&gt; findAll(); // (3)

  long count();  // (4)

  void delete(T entity);  // (5)

  boolean existsById(ID primaryKey);  // (6)

  // … more functionality omitted.
}</code></pre>
<p>(1) 지정된 엔터티를 저장합니다. 
(2) 지정된 ID로 식별되는 엔터티를 반환합니다. 
(3) 모든 엔터티를 반환합니다. 
(4) 엔터티 수를 반환합니다. 
(5) 지정된 엔터티를 삭제합니다. 
(6) 지정된 ID를 가진 엔터티가 존재하는지 여부를 나타냅니다.</p>
<p>이 인터페이스에 선언된 메서드를 일반적으로 CRUD 메서드라고 합니다. <code>ListCrudRepository</code>는 동등한 메소드를 제공하지만 <code>CrudRepository</code> 메소드가 <code>Iterable</code>을 반환하는 <code>List</code>를 반환합니다.</p>
<blockquote>
<p><strong>[Note]</strong>
또한 <code>JpaRepository</code> 또는 <code>MongoRepository</code>와 같은 지속성 기술별 추상화도 제공합니다. 이러한 인터페이스는 <code>CrudRepository</code>를 확장하고 <code>CrudRepository</code>와 같이 다소 일반적인 지속성 기술(persistence technology)에 구애받지 않는 인터페이스 외에도 기본 지속성 기술의 기능을 노출합니다.</p>
</blockquote>
<p><code>CrudRepository</code> 외에도 엔터티에 대한 페이지 매김 액세스를 쉽게 하기 위해 추가 메서드를 추가하는 <a href="https://docs.spring.io/spring-data/commons/docs/3.2.5/api//org/springframework/data/repository/PagingAndSortingRepository.html"><code>PagingAndSortingRepository</code></a> 및 <a href="https://docs.spring.io/spring-data/commons/docs/3.2.5/api//org/springframework/data/repository/ListPagingAndSortingRepository.html"><code>ListPagingAndSortingRepository</code></a>가 있습니다.</p>
<p>PagingAndSortingRepository interface</p>
<pre><code class="language-java">public interface PagingAndSortingRepository&lt;T, ID&gt;  {

  Iterable&lt;T&gt; findAll(Sort sort);

  Page&lt;T&gt; findAll(Pageable pageable);
}</code></pre>
<blockquote>
<p><strong>[Note]</strong>
확장 인터페이스는 실제 저장소 모듈에서 지원될 수 있습니다. 이 문서에서는 일반적인 계획(scheme)을 설명하지만 저장소 모듈이 사용하려는 인터페이스를 지원하는지 확인하십시오.</p>
</blockquote>
<p>페이지 크기 20으로 <code>User</code>의 두 번째 페이지에 액세스하려면 다음과 같이 할 수 있습니다.</p>
<pre><code class="language-java">PagingAndSortingRepository&lt;User, Long&gt; repository = // … get access to a bean
Page&lt;User&gt; users = repository.findAll(PageRequest.of(1, 20));</code></pre>
<p><code>ListPagingAndSortingRepository</code>는 동등한 메서드를 제공하지만 <code>PagingAndSortingRepository</code> 메서드가 <code>Iterable</code>을 반환하는 <code>List</code>를 반환합니다.</p>
<p>쿼리 방식 외에도 개수 쿼리와 삭제 쿼리 모두에 대한 쿼리 파생이 가능합니다. 다음 목록은 파생된 카운트 쿼리에 대한 인터페이스 정의를 보여줍니다.</p>
<p>Derived Count Query</p>
<pre><code class="language-java">interface UserRepository extends CrudRepository&lt;User, Long&gt; {

  long countByLastname(String lastname);
}</code></pre>
<p>다음 목록은 파생된 삭제 쿼리에 대한 인터페이스 정의를 보여줍니다.</p>
<p>Derived Delete Query</p>
<pre><code class="language-java">interface UserRepository extends CrudRepository&lt;User, Long&gt; {

  long deleteByLastname(String lastname);

  List&lt;User&gt; removeByLastname(String lastname);
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Getting Started]]></title>
            <link>https://velog.io/@dev_hammy/Getting-Started-cy82ugah</link>
            <guid>https://velog.io/@dev_hammy/Getting-Started-cy82ugah</guid>
            <pubDate>Fri, 19 Apr 2024 14:29:12 GMT</pubDate>
            <description><![CDATA[<p>작업 환경을 부트스트랩 설정하는 쉬운 방법은 start.spring.io를 통해 Spring 기반 프로젝트를 생성하거나 <a href="https://spring.io/tools">Spring Tools</a>에서 Spring 프로젝트를 생성하는 것입니다.</p>
<h1 id="examples-repository">Examples Repository</h1>
<p>GitHub <a href="https://github.com/spring-projects/spring-data-examples">spring-data-examples 리포지토리</a>에는 라이브러리 작동 방식을 파악하기 위해 다운로드하고 사용해 볼 수 있는 몇 가지 예제가 있습니다.</p>
<h1 id="hello-world">Hello World</h1>
<p>간단한 엔터티와 해당 저장소부터 시작해 보겠습니다.</p>
<pre><code class="language-java">@Entity
class Person {

  @Id @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;
  private String name;

  // getters and setters omitted for brevity
}

interface PersonRepository extends Repository&lt;Person, Long&gt; {

  Person save(Person person);

  Optional&lt;Person&gt; findById(long id);
}</code></pre>
<p>다음 예제와 같이 실행할 기본 애플리케이션을 만듭니다.</p>
<pre><code class="language-java">@SpringBootApplication
public class DemoApplication {

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

  @Bean
  CommandLineRunner runner(PersonRepository repository) {
    return args -&gt; {

      Person person = new Person();
      person.setName(&quot;John&quot;);

      repository.save(person);
      Person saved = repository.findById(person.getId()).orElseThrow(NoSuchElementException::new);
    };
  }
}</code></pre>
<p>이 간단한 예에서도 지적해야 할 몇 가지 주목할만한 사항이 있습니다.</p>
<ul>
<li><p>리포지토리 인스턴스가 자동으로 구현됩니다. <code>@Bean</code> 메소드의 매개변수로 사용되면 추가 주석이 필요 없이 자동으로 연결됩니다.</p>
</li>
<li><p>기본 저장소는 <code>Repository</code>를 확장합니다. 애플리케이션에 노출하려는 API 표면의 양을 고려하는 것이 좋습니다. 더 복잡한 저장소 인터페이스는 <code>ListCrudRepository</code> 또는 <code>JpaRepository</code>입니다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Programmatic Creation of @AspectJ Proxies]]></title>
            <link>https://velog.io/@dev_hammy/Programmatic-Creation-of-AspectJ-Proxies</link>
            <guid>https://velog.io/@dev_hammy/Programmatic-Creation-of-AspectJ-Proxies</guid>
            <pubDate>Wed, 17 Apr 2024 05:46:56 GMT</pubDate>
            <description><![CDATA[<p><code>&lt;aop:config&gt;</code> 또는 <code>&lt;aop:aspectj-autoproxy&gt;</code>를 사용하여 구성에서 aspect을 선언하는 것 외에도 대상 객체에 advice하는 프록시를 프로그래밍 방식으로 생성하는 것도 가능합니다. Spring의 AOP API에 대한 자세한 내용은 <a href="https://docs.spring.io/spring-framework/reference/core/aop-api.html">다음 장</a>을 참조하세요. 여기서는 @AspectJ aspect을 사용하여 자동으로 프록시를 생성하는 기능에 중점을 두고 싶습니다.</p>
<p>하나 이상의 @AspectJ aspect에서 advice하는 대상 객체에 대한 프록시를 생성하기 위해 <code>org.springframework.aop.aspectj.annotation.AspectJProxyFactory</code> 클래스를 사용할 수 있습니다. 이 클래스의 기본 사용법은 다음 예제와 같이 매우 간단합니다.</p>
<pre><code class="language-java">// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);

// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);

// you can also add existing aspect instances, the type of the object supplied
// must be an @AspectJ aspect
factory.addAspect(usageTracker);

// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();</code></pre>
<p>자세한 내용은 <a href="https://docs.spring.io/spring-framework/docs/6.1.6/javadoc-api/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.html">javadoc</a>을 참조하세요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Proxying Mechanisms]]></title>
            <link>https://velog.io/@dev_hammy/Proxying-Mechanisms</link>
            <guid>https://velog.io/@dev_hammy/Proxying-Mechanisms</guid>
            <pubDate>Wed, 17 Apr 2024 05:45:42 GMT</pubDate>
            <description><![CDATA[<p>Spring AOP는 JDK 동적 프록시 또는 CGLIB를 사용하여 주어진 대상 객체에 대한 프록시를 생성합니다. JDK 동적 프록시는 JDK에 내장된 반면 CGLIB는 일반적인 오픈 소스 클래스 정의 라이브러리(<code>spring-core</code>로 다시 패키징됨)입니다.</p>
<p>프록시할 대상 개체가 하나 이상의 인터페이스를 구현하는 경우 JDK 동적 프록시가 사용됩니다. 대상 유형으로 구현된 모든 인터페이스가 프록시됩니다. 대상 객체가 인터페이스를 구현하지 않으면 CGLIB 프록시가 생성됩니다.</p>
<p>CGLIB 프록싱을 강제로 사용하려는 경우(예를 들어 인터페이스에 의해 구현된 메소드뿐만 아니라 대상 객체에 대해 정의된 모든 메소드를 프록시하려는 경우) 그렇게 할 수 있습니다. 그러나 다음 문제를 고려해야 합니다.</p>
<ul>
<li><p>CGLIB를 사용하면 런타임에서 생성된 하위 클래스에서 재정의할 수 없기 때문에 <code>final</code> 메서드를 advice할 수 없습니다.</p>
</li>
<li><p>Spring 4.0부터 CGLIB 프록시 인스턴스가 Objenesis를 통해 생성되므로 프록시 객체의 생성자는 더 이상 두 번 호출되지 않습니다. JVM이 생성자 우회를 허용하지 않는 경우에만 Spring의 AOP 지원에서 이중 호출 및 해당 디버그 로그 항목을 볼 수 있습니다.</p>
</li>
</ul>
<p>CGLIB 프록시를 강제로 사용하려면 다음과 같이 <code>&lt;aop:config&gt;</code> 요소의 <code>proy-target-class</code> 속성 값을 <code>true</code>로 설정하세요.</p>
<pre><code class="language-xml">&lt;aop:config proxy-target-class=&quot;true&quot;&gt;
    &lt;!-- other beans defined here... --&gt;
&lt;/aop:config&gt;</code></pre>
<p>AspectJ 자동 프록시 지원을 사용할 때 CGLIB 프록시를 강제하려면 다음과 같이 <code>&lt;aop:aspectj-autoproxy&gt;</code> 요소의 <code>proxy-target-class</code> 속성을 <code>true</code>로 설정하세요.</p>
<pre><code class="language-xml">&lt;aop:aspectj-autoproxy proxy-target-class=&quot;true&quot;/&gt;</code></pre>
<blockquote>
<p><strong>[Note]</strong>
여러 <code>&lt;aop:config/&gt;</code> 섹션은 런타임 시 단일 통합 자동 프록시 생성자로 축소되며, 이는 <code>&lt;aop:config/&gt;</code> 섹션(일반적으로 다른 XML Bean 정의 파일에서)이 지정한 가장 강력한 프록시 설정을 적용합니다. 이는 <code>&lt;tx:annotation-driven/&gt;</code> 및 <code>&lt;aop:aspectj-autoproxy/&gt;</code> 요소에도 적용됩니다.</p>
<p>명확하게 말하면, <code>&lt;tx:annotation-driven/&gt;</code>, <code>&lt;aop:aspectj-autoproxy/&gt;</code> 또는 <code>&lt;aop:config/&gt;</code> 요소에서 <code>proxy-target-class=&quot;true&quot;</code>를 사용하면 세 가지 모두에 대해 CGLIB 프록시를 강제로 사용하게 됩니다.</p>
</blockquote>
<h1 id="understanding-aop-proxies">Understanding AOP Proxies</h1>
<p>Spring AOP는 프록시 기반입니다. 자신만의 aspect을 작성하거나 Spring Framework와 함께 제공되는 Spring AOP 기반 aspect을 사용하기 전에 마지막 문장이 실제로 의미하는 바의 의미를 파악하는 것이 매우 중요합니다.</p>
<p>먼저 다음 코드 조각에서 볼 수 있듯이 일반 바닐라, 프록시되지 않은, 특별한 내용이 없는 객체 참조가 있는 시나리오를 생각해 보세요.</p>
<pre><code class="language-java">public class SimplePojo implements Pojo {

    public void foo() {
        // this next method invocation is a direct call on the &#39;this&#39; reference
        this.bar();
    }

    public void bar() {
        // some logic...
    }
}</code></pre>
<p>객체 참조에 대해 메서드를 호출하면 다음 이미지와 목록에 표시된 것처럼 해당 객체 참조에서 메서드가 직접 호출됩니다.</p>
<p><img src="https://docs.spring.io/spring-framework/reference/_images/aop-proxy-plain-pojo-call.png" alt=""></p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        Pojo pojo = new SimplePojo();
        // this is a direct method call on the &#39;pojo&#39; reference
        pojo.foo();
    }
}</code></pre>
<p>클라이언트 코드가 갖는 참조가 프록시인 경우 상황이 약간 변경됩니다. 다음 다이어그램과 코드 조각을 고려하세요.</p>
<p><img src="https://docs.spring.io/spring-framework/reference/_images/aop-proxy-call.png" alt=""></p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}</code></pre>
<p>여기서 이해해야 할 중요한 점은 <code>Main</code> 클래스의 <code>main(..)</code> 메서드 내부에 있는 클라이언트 코드에 프록시에 대한 참조가 있다는 것입니다. 이는 해당 객체 참조에 대한 메서드 호출이 프록시에 대한 호출임을 의미합니다. 결과적으로 프록시는 특정 메서드 호출과 관련된 모든 인터셉터(어드바이스)에 위임할 수 있습니다. 그러나 호출이 최종적으로 대상 객체(이 경우 <code>SimplePojo</code> 참조)에 도달하면 <code>this.bar()</code> 또는 <code>this.foo()</code>와 같이 자체적으로 수행할 수 있는 모든 메서드 호출이 호출됩니다. 프록시가 아닌 <code>this</code> 참조입니다. 이는 중요한 의미를 갖습니다. 이는 자체 호출로 인해 메서드 호출과 관련된 advice이 실행될 기회를 얻지 못한다는 것을 의미합니다.</p>
<p>좋습니다. 그러면 이에 대해 어떻게 해야 합니까? 가장 좋은 접근 방식(여기서는 &quot;최고&quot;라는 용어가 느슨하게 사용됨)은 자체 호출이 발생하지 않도록 코드를 리팩터링하는 것입니다. 이를 위해서는 귀하의 노력이 수반되지만 가장 좋고 최소 침습적 접근 방식입니다. 다음 접근 방식은 정말 끔찍합니다. 우리는 그것을 지적하기를 주저합니다. 왜냐하면 그것이 너무 끔찍하기 때문입니다. 다음 예제에서 볼 수 있듯이 (우리에게는 고통스럽기는 하지만) 클래스 내의 로직을 Spring AOP에 완전히 연결할 수 있습니다.</p>
<pre><code class="language-java">public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}</code></pre>
<p>이는 코드를 Spring AOP에 완전히 연결하고 클래스 자체가 AOP에 맞서는 AOP 컨텍스트에서 사용되고 있다는 사실을 인식하게 합니다. 또한 다음 예제와 같이 프록시를 생성할 때 몇 가지 추가 구성이 필요합니다.</p>
<pre><code class="language-java">public class Main {

    public static void main(String[] args) {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);

        Pojo pojo = (Pojo) factory.getProxy();
        // this is a method call on the proxy!
        pojo.foo();
    }
}</code></pre>
<p>마지막으로 AspectJ는 프록시 기반 AOP 프레임워크가 아니기 때문에 이러한 자체 호출 문제가 없다는 점에 유의해야 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Mixing Aspect Types]]></title>
            <link>https://velog.io/@dev_hammy/Mixing-Aspect-Types</link>
            <guid>https://velog.io/@dev_hammy/Mixing-Aspect-Types</guid>
            <pubDate>Wed, 17 Apr 2024 04:41:59 GMT</pubDate>
            <description><![CDATA[<p>자동 프록시 지원, 스키마 정의 <code>&lt;aop:aspect&gt;</code> aspect, <code>&lt;aop:advisor&gt;</code> 선언된 advisor, 심지어 동일한 구성에서 다른 스타일의 프록시 및 인터셉터를 사용하여 @AspectJ 스타일 aspect을 혼합하는 것이 완벽하게 가능합니다. 이들 모두는 동일한 기본 지원 메커니즘을 사용하여 구현되며 아무런 어려움 없이 공존할 수 있습니다.</p>
]]></description>
        </item>
    </channel>
</rss>