<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발</title>
    <link>https://devsite.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 11 May 2026 21:50:12 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>shaprimanDev</managingEditor>
    <image>
      <title>개발</title>
      <url>https://tistory1.daumcdn.net/tistory/6351449/attach/479aa6d2ead5499cb783ca1779b30b76</url>
      <link>https://devsite.tistory.com</link>
    </image>
    <item>
      <title>오라클 함수(Function) 관리 Best Practice - 실무 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%95%A8%EC%88%98Function-%EA%B4%80%EB%A6%AC-Best-Practice-%EC%8B%A4%EB%AC%B4-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 함수(Function)는 특정 값을 반환하는 PL/SQL 서브프로그램으로, SQL 쿼리 내에서 직접 호출할 수 있다는 점에서 프로시저와 차별화됩니다. 이 가이드에서는 함수의 조회, 관리, 최적화까지 실무에서 필요한 모든 내용을 다룹니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#1-%ED%95%A8%EC%88%98-%EA%B8%B0%EB%B3%B8-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C&quot;&gt;함수 기본 정보 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-%ED%95%A8%EC%88%98-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EB%B0%8F-%EB%B0%98%ED%99%98%EA%B0%92-%EC%A1%B0%ED%9A%8C&quot;&gt;함수 파라미터 및 반환값 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-%ED%95%A8%EC%88%98-%EC%86%8C%EC%8A%A4-%EC%BD%94%EB%93%9C-%EC%A1%B0%ED%9A%8C&quot;&gt;함수 소스 코드 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-%ED%95%A8%EC%88%98-%EC%84%B1%EB%8A%A5-%EB%B6%84%EC%84%9D&quot;&gt;함수 성능 분석&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-deterministic-%ED%95%A8%EC%88%98-%EA%B4%80%EB%A6%AC&quot;&gt;DETERMINISTIC 함수 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-%ED%95%A8%EC%88%98-%EC%9D%98%EC%A1%B4%EC%84%B1-%EB%B0%8F-%EC%98%81%ED%96%A5%EB%8F%84-%EB%B6%84%EC%84%9D&quot;&gt;함수 의존성 및 영향도 분석&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-%ED%95%A8%EC%88%98-%EC%9E%91%EC%84%B1-best-practices&quot;&gt;함수 작성 Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-%ED%95%A8%EC%88%98-vs-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80-%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C&quot;&gt;함수 vs 프로시저 선택 가이드&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 함수 기본 정보 조회&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 존재 여부 및 상태 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    OBJECT_NAME,
    OBJECT_TYPE,
    STATUS,
    CREATED,
    LAST_DDL_TIME
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'FUNCTION'
  AND OBJECT_NAME = UPPER('함수명');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 컬럼:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;STATUS&lt;/code&gt;: VALID(정상) / INVALID(오류)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATED&lt;/code&gt;: 최초 생성일&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LAST_DDL_TIME&lt;/code&gt;: 마지막 변경일&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 함수 목록 조회&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    OBJECT_NAME AS &quot;함수명&quot;,
    STATUS AS &quot;상태&quot;,
    TO_CHAR(CREATED, 'YYYY-MM-DD') AS &quot;생성일&quot;,
    TO_CHAR(LAST_DDL_TIME, 'YYYY-MM-DD HH24:MI:SS') AS &quot;최종수정일&quot;
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'FUNCTION'
ORDER BY OBJECT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스키마별 함수 통계&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    OWNER AS &quot;스키마&quot;,
    COUNT(*) AS &quot;함수개수&quot;,
    SUM(CASE WHEN STATUS = 'VALID' THEN 1 ELSE 0 END) AS &quot;정상&quot;,
    SUM(CASE WHEN STATUS = 'INVALID' THEN 1 ELSE 0 END) AS &quot;오류&quot;
FROM ALL_OBJECTS
WHERE OBJECT_TYPE = 'FUNCTION'
GROUP BY OWNER
ORDER BY COUNT(*) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 함수 파라미터 및 반환값 조회&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 시그니처 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    ARGUMENT_NAME,
    POSITION,
    DATA_TYPE,
    IN_OUT,
    DATA_LENGTH,
    DATA_PRECISION,
    DATA_SCALE,
    DEFAULT_VALUE
FROM ALL_ARGUMENTS
WHERE OBJECT_NAME = UPPER('함수명')
  AND PACKAGE_NAME IS NULL  -- 패키지 함수 제외
ORDER BY POSITION;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반환값 식별:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;POSITION = 0&lt;/code&gt;: 함수의 반환값&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARGUMENT_NAME IS NULL&lt;/code&gt;: 반환값 (일부 케이스)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패키지 내 함수 파라미터 조회&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    OBJECT_NAME AS &quot;패키지명&quot;,
    SUBPROGRAM_ID AS &quot;서브프로그램ID&quot;,
    ARGUMENT_NAME AS &quot;파라미터명&quot;,
    POSITION,
    DATA_TYPE,
    IN_OUT
FROM ALL_ARGUMENTS
WHERE PACKAGE_NAME = UPPER('패키지명')
  AND OBJECT_NAME = UPPER('함수명')
ORDER BY POSITION;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 상세 정보 (통합 뷰)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    p.OBJECT_NAME AS &quot;함수명&quot;,
    p.PROCEDURE_NAME,
    a.ARGUMENT_NAME AS &quot;파라미터명&quot;,
    a.POSITION AS &quot;순서&quot;,
    a.DATA_TYPE AS &quot;데이터타입&quot;,
    a.IN_OUT AS &quot;입출력&quot;,
    a.DEFAULT_VALUE AS &quot;기본값&quot;
FROM ALL_PROCEDURES p
LEFT JOIN ALL_ARGUMENTS a
    ON p.OBJECT_NAME = a.OBJECT_NAME
WHERE p.OBJECT_TYPE = 'FUNCTION'
  AND p.OBJECT_NAME = UPPER('함수명')
ORDER BY a.POSITION;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 함수 소스 코드 조회&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 소스 코드 보기&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    LINE,
    TEXT
FROM ALL_SOURCE
WHERE NAME = UPPER('함수명')
  AND TYPE = 'FUNCTION'
ORDER BY LINE;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소스 코드를 하나의 문자열로 조회&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
    LISTAGG(TEXT, '') WITHIN GROUP (ORDER BY LINE) AS SOURCE_CODE
FROM USER_SOURCE
WHERE NAME = UPPER('함수명')
  AND TYPE = 'FUNCTION';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특정 키워드를 포함한 함수 찾기&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DISTINCT
    NAME AS &quot;함수명&quot;,
    COUNT(*) AS &quot;매칭라인수&quot;
FROM ALL_SOURCE
WHERE TYPE = 'FUNCTION'
  AND UPPER(TEXT) LIKE '%키워드%'
GROUP BY NAME
ORDER BY COUNT(*) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 활용:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 테이블을 사용하는 함수 찾기&lt;/li&gt;
&lt;li&gt;특정 함수를 호출하는 다른 함수 찾기&lt;/li&gt;
&lt;li&gt;하드코딩된 값 찾기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 함수 성능 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 내에서 호출된 함수 성능 확인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    sql_text,
    executions AS &quot;실행횟수&quot;,
    ROUND(elapsed_time/1000000, 2) AS &quot;총소요시간(초)&quot;,
    ROUND(elapsed_time/executions/1000000, 4) AS &quot;평균소요시간(초)&quot;,
    ROUND(cpu_time/1000000, 2) AS &quot;CPU시간(초)&quot;
FROM v$sql
WHERE UPPER(sql_text) LIKE '%함수명%'
  AND executions &amp;gt; 0
ORDER BY elapsed_time DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 호출 빈도 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- SQL 문장에서 함수 호출 빈도
SELECT
    sql_text,
    executions,
    (LENGTH(sql_text) - LENGTH(REPLACE(UPPER(sql_text), UPPER('함수명'), ''))) 
    / LENGTH('함수명') AS &quot;함수호출추정횟수&quot;
FROM v$sql
WHERE UPPER(sql_text) LIKE '%함수명%'
ORDER BY executions DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;느린 함수 식별&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 소스 코드 길이로 복잡도 추정
SELECT
    NAME AS &quot;함수명&quot;,
    COUNT(*) AS &quot;코드라인수&quot;,
    MAX(LINE) AS &quot;최대라인번호&quot;
FROM USER_SOURCE
WHERE TYPE = 'FUNCTION'
GROUP BY NAME
HAVING COUNT(*) &amp;gt; 100  -- 100줄 이상
ORDER BY COUNT(*) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. DETERMINISTIC 함수 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DETERMINISTIC 속성 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DETERMINISTIC 함수는 같은 입력에 대해 항상 같은 결과를 반환하므로 오라클이 결과를 캐싱할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 소스에서 DETERMINISTIC 키워드 확인
SELECT
    NAME AS &quot;함수명&quot;,
    CASE 
        WHEN MAX(CASE WHEN UPPER(TEXT) LIKE '%DETERMINISTIC%' THEN 1 ELSE 0 END) = 1 
        THEN 'YES' 
        ELSE 'NO' 
    END AS &quot;DETERMINISTIC&quot;
FROM USER_SOURCE
WHERE TYPE = 'FUNCTION'
GROUP BY NAME
ORDER BY NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DETERMINISTIC 함수 작성 예시&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- ✅ 좋은 예: 순수 계산 함수
CREATE OR REPLACE FUNCTION calculate_tax(
    p_amount IN NUMBER
) RETURN NUMBER
DETERMINISTIC
IS
BEGIN
    RETURN p_amount * 0.1;  -- 항상 같은 결과
END;
/

-- ❌ 나쁜 예: 시간 의존적 함수
CREATE OR REPLACE FUNCTION get_current_discount(
    p_amount IN NUMBER
) RETURN NUMBER
DETERMINISTIC  -- 잘못된 사용!
IS
BEGIN
    IF SYSDATE BETWEEN '01-JAN-23' AND '31-DEC-23' THEN
        RETURN p_amount * 0.2;
    ELSE
        RETURN p_amount * 0.1;
    END IF;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DETERMINISTIC 사용 조건:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;입력 파라미터만으로 결과가 결정&lt;/li&gt;
&lt;li&gt;데이터베이스 조회 없음&lt;/li&gt;
&lt;li&gt;시스템 함수(SYSDATE, USER 등) 사용 없음&lt;/li&gt;
&lt;li&gt;시퀀스 사용 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 함수 의존성 및 영향도 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수가 참조하는 객체 확인&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
    NAME AS &quot;함수명&quot;,
    TYPE AS &quot;타입&quot;,
    REFERENCED_OWNER AS &quot;참조스키마&quot;,
    REFERENCED_NAME AS &quot;참조객체&quot;,
    REFERENCED_TYPE AS &quot;참조타입&quot;,
    REFERENCED_LINK_NAME AS &quot;DB링크&quot;
FROM USER_DEPENDENCIES
WHERE NAME = UPPER('함수명')
  AND TYPE = 'FUNCTION'
ORDER BY REFERENCED_TYPE, REFERENCED_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수를 참조하는 객체 확인 (역방향)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    NAME AS &quot;참조하는객체&quot;,
    TYPE AS &quot;객체타입&quot;,
    REFERENCED_NAME AS &quot;함수명&quot;
FROM USER_DEPENDENCIES
WHERE REFERENCED_NAME = UPPER('함수명')
  AND REFERENCED_TYPE = 'FUNCTION'
ORDER BY TYPE, NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 변경 시 영향받는 함수 찾기&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT DISTINCT
    d.NAME AS &quot;영향받는함수&quot;,
    d.TYPE AS &quot;객체타입&quot;,
    d.REFERENCED_NAME AS &quot;의존테이블&quot;
FROM USER_DEPENDENCIES d
WHERE d.REFERENCED_NAME = UPPER('테이블명')
  AND d.REFERENCED_TYPE = 'TABLE'
  AND d.TYPE = 'FUNCTION'
ORDER BY d.NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 의존성 체인 분석&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 3단계 의존성까지 추적
WITH dep_tree AS (
    -- Level 1: 직접 의존성
    SELECT 
        NAME AS func_name,
        REFERENCED_NAME AS depends_on,
        1 AS depth
    FROM USER_DEPENDENCIES
    WHERE NAME = UPPER('함수명')
      AND TYPE = 'FUNCTION'

    UNION ALL

    -- Level 2: 간접 의존성
    SELECT 
        dt.func_name,
        d.REFERENCED_NAME,
        2
    FROM dep_tree dt
    JOIN USER_DEPENDENCIES d
        ON dt.depends_on = d.NAME
    WHERE dt.depth = 1
      AND d.TYPE = 'FUNCTION'

    UNION ALL

    -- Level 3
    SELECT 
        dt.func_name,
        d.REFERENCED_NAME,
        3
    FROM dep_tree dt
    JOIN USER_DEPENDENCIES d
        ON dt.depends_on = d.NAME
    WHERE dt.depth = 2
      AND d.TYPE = 'FUNCTION'
)
SELECT DISTINCT
    func_name AS &quot;함수명&quot;,
    depends_on AS &quot;의존객체&quot;,
    depth AS &quot;의존깊이&quot;
FROM dep_tree
ORDER BY depth, depends_on;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 함수 작성 Best Practices&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 명명 규칙&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;-- 권장 명명 규칙
-- FN_[동사]_[명사] 또는 GET_[명사], CALC_[명사]
-- 예시:
-- - FN_GET_EMPLOYEE_NAME
-- - FN_CALC_TAX
-- - FN_CHECK_VALID_DATE
-- - GET_DEPARTMENT_TOTAL&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 RESULT_CACHE 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 11g 이상에서는 RESULT_CACHE를 사용하여 함수 결과를 메모리에 캐싱할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- ✅ 좋은 예: 자주 호출되고 변경이 드문 데이터
CREATE OR REPLACE FUNCTION get_department_name(
    p_dept_id IN NUMBER
) RETURN VARCHAR2
RESULT_CACHE
IS
    v_dept_name VARCHAR2(100);
BEGIN
    SELECT department_name
    INTO v_dept_name
    FROM departments
    WHERE department_id = p_dept_id;

    RETURN v_dept_name;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RESULT_CACHE 사용 조건:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수가 자주 호출됨&lt;/li&gt;
&lt;li&gt;입력값 조합이 제한적&lt;/li&gt;
&lt;li&gt;참조하는 데이터가 자주 변경되지 않음&lt;/li&gt;
&lt;li&gt;SELECT 문만 있고 DML이 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 예외 처리 패턴&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- ✅ 좋은 예: 명확한 예외 처리
CREATE OR REPLACE FUNCTION get_employee_salary(
    p_emp_id IN NUMBER
) RETURN NUMBER
IS
    v_salary NUMBER;
BEGIN
    SELECT salary
    INTO v_salary
    FROM employees
    WHERE employee_id = p_emp_id;

    RETURN v_salary;

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        -- 로그 기록 또는 기본값 반환
        RETURN 0;
    WHEN TOO_MANY_ROWS THEN
        -- 데이터 정합성 문제
        RAISE_APPLICATION_ERROR(-20001, 
            '직원 ID ' || p_emp_id || '에 대한 중복 데이터 존재');
    WHEN OTHERS THEN
        -- 예상치 못한 오류
        RAISE_APPLICATION_ERROR(-20002, 
            '함수 실행 오류: ' || SQLERRM);
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.4 NULL 처리&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- ✅ 좋은 예: NULL 안전 함수
CREATE OR REPLACE FUNCTION calculate_discount(
    p_amount IN NUMBER,
    p_rate IN NUMBER DEFAULT 0
) RETURN NUMBER
IS
BEGIN
    -- NULL 체크
    IF p_amount IS NULL THEN
        RETURN NULL;
    END IF;

    IF p_rate IS NULL OR p_rate &amp;lt; 0 THEN
        RETURN p_amount;  -- 할인 없음
    END IF;

    RETURN p_amount * (1 - p_rate);
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.5 함수 순수성 유지 (Purity)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- ✅ 좋은 예: 순수 함수 (사이드 이펙트 없음)
CREATE OR REPLACE FUNCTION format_phone_number(
    p_phone IN VARCHAR2
) RETURN VARCHAR2
DETERMINISTIC
IS
BEGIN
    RETURN SUBSTR(p_phone, 1, 3) || '-' || 
           SUBSTR(p_phone, 4, 4) || '-' || 
           SUBSTR(p_phone, 8, 4);
END;
/

-- ❌ 나쁜 예: 사이드 이펙트 있음
CREATE OR REPLACE FUNCTION log_and_return(
    p_value IN NUMBER
) RETURN NUMBER
IS
    PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
    -- 함수에서 데이터 변경 (안티패턴)
    INSERT INTO audit_log VALUES (SYSDATE, p_value);
    COMMIT;

    RETURN p_value;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.6 성능을 위한 SQL 최적화&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- ❌ 나쁜 예: 함수 내에서 반복 쿼리
CREATE OR REPLACE FUNCTION get_total_orders_bad(
    p_customer_id IN NUMBER
) RETURN NUMBER
IS
    v_total NUMBER := 0;
BEGIN
    FOR rec IN (SELECT order_id FROM orders WHERE customer_id = p_customer_id) LOOP
        SELECT amount INTO v_total
        FROM order_details
        WHERE order_id = rec.order_id;
    END LOOP;

    RETURN v_total;
END;
/

-- ✅ 좋은 예: 단일 쿼리로 처리
CREATE OR REPLACE FUNCTION get_total_orders_good(
    p_customer_id IN NUMBER
) RETURN NUMBER
IS
    v_total NUMBER;
BEGIN
    SELECT SUM(od.amount)
    INTO v_total
    FROM orders o
    JOIN order_details od ON o.order_id = od.order_id
    WHERE o.customer_id = p_customer_id;

    RETURN NVL(v_total, 0);
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 함수 vs 프로시저 선택 가이드&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 함수를 사용할까?&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;프로시저&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQL 쿼리 내에서 호출&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단일 값 반환&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;△ (OUT 파라미터)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELECT 문에서 사용&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DML 수행 (INSERT/UPDATE/DELETE)&lt;/td&gt;
&lt;td&gt;❌ 권장하지 않음&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복잡한 비즈니스 로직&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트랜잭션 제어 (COMMIT/ROLLBACK)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 사용이 적합한 경우&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- ✅ Case 1: SQL에서 직접 사용
SELECT 
    employee_id,
    first_name,
    FN_GET_DEPARTMENT_NAME(department_id) AS dept_name,
    FN_CALC_TAX(salary) AS tax_amount
FROM employees;

-- ✅ Case 2: 재사용 가능한 계산 로직
SELECT 
    product_id,
    price,
    FN_APPLY_DISCOUNT(price, discount_rate) AS discounted_price
FROM products;

-- ✅ Case 3: 데이터 변환/포맷팅
SELECT 
    order_id,
    FN_FORMAT_DATE(order_date, 'YYYY-MM-DD') AS formatted_date
FROM orders;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로시저 사용이 적합한 경우&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- ✅ Case 1: 복잡한 비즈니스 로직
BEGIN
    SP_PROCESS_MONTHLY_SALARY(
        p_year =&amp;gt; 2024,
        p_month =&amp;gt; 12
    );
END;
/

-- ✅ Case 2: 다중 DML 작업
BEGIN
    SP_TRANSFER_EMPLOYEE(
        p_emp_id =&amp;gt; 100,
        p_new_dept_id =&amp;gt; 50,
        p_effective_date =&amp;gt; SYSDATE
    );
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;9. 함수 성능 최적화 체크리스트&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 PARALLEL_ENABLE 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대량 데이터 처리 시 병렬 실행을 허용합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION calculate_bonus(
    p_salary IN NUMBER
) RETURN NUMBER
PARALLEL_ENABLE
IS
BEGIN
    RETURN p_salary * 0.1;
END;
/

-- 병렬 실행 가능
SELECT 
    employee_id,
    calculate_bonus(salary) AS bonus
FROM employees;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 함수 기반 인덱스 (FBI)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용되는 함수에 대해 인덱스를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 함수 생성
CREATE OR REPLACE FUNCTION get_upper_name(
    p_name IN VARCHAR2
) RETURN VARCHAR2
DETERMINISTIC
IS
BEGIN
    RETURN UPPER(p_name);
END;
/

-- 함수 기반 인덱스 생성
CREATE INDEX idx_upper_name 
ON employees(get_upper_name(first_name));

-- 인덱스 활용 쿼리
SELECT * 
FROM employees
WHERE get_upper_name(first_name) = 'JOHN';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.3 불필요한 함수 호출 제거&lt;/h3&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;-- ❌ 나쁜 예: 같은 함수를 여러 번 호출
SELECT 
    employee_id,
    FN_GET_DEPT_NAME(department_id),
    FN_GET_DEPT_LOCATION(department_id),
    FN_GET_DEPT_MANAGER(department_id)
FROM employees;

-- ✅ 좋은 예: JOIN으로 한 번에 조회
SELECT 
    e.employee_id,
    d.department_name,
    d.location,
    d.manager_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;10. 함수 모니터링 및 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INVALID 함수 찾기 및 재컴파일&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- INVALID 함수 목록
SELECT
    OBJECT_NAME AS &quot;함수명&quot;,
    STATUS AS &quot;상태&quot;,
    LAST_DDL_TIME AS &quot;최종수정일&quot;
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'FUNCTION'
  AND STATUS = 'INVALID'
ORDER BY LAST_DDL_TIME DESC;

-- 컴파일 오류 확인
SELECT
    LINE,
    POSITION,
    TEXT AS &quot;오류내용&quot;
FROM USER_ERRORS
WHERE NAME = UPPER('함수명')
  AND TYPE = 'FUNCTION'
ORDER BY SEQUENCE;

-- 재컴파일 스크립트 생성
SELECT 'ALTER FUNCTION ' || OBJECT_NAME || ' COMPILE;'
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'FUNCTION'
  AND STATUS = 'INVALID';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 사용 현황 모니터링&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 어떤 프로그램에서 함수를 사용하는지 확인
SELECT DISTINCT
    d.NAME AS &quot;호출하는객체&quot;,
    d.TYPE AS &quot;객체타입&quot;,
    o.STATUS AS &quot;상태&quot;
FROM USER_DEPENDENCIES d
JOIN USER_OBJECTS o ON d.NAME = o.OBJECT_NAME
WHERE d.REFERENCED_NAME = UPPER('함수명')
  AND d.REFERENCED_TYPE = 'FUNCTION'
ORDER BY d.TYPE, d.NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수 백업 (DDL 추출)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 개별 함수 DDL
SELECT DBMS_METADATA.GET_DDL('FUNCTION', '함수명') AS ddl
FROM DUAL;

-- 모든 함수 DDL 추출
SELECT DBMS_METADATA.GET_DDL('FUNCTION', OBJECT_NAME) AS ddl
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'FUNCTION'
ORDER BY OBJECT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;11. 함수 테스트 및 디버깅&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단위 테스트 템플릿&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 테스트 케이스 테이블
CREATE TABLE function_test_cases (
    test_id NUMBER PRIMARY KEY,
    function_name VARCHAR2(100),
    test_case VARCHAR2(500),
    input_params VARCHAR2(4000),
    expected_output VARCHAR2(4000),
    actual_output VARCHAR2(4000),
    test_result VARCHAR2(10),
    test_date DATE
);

-- 테스트 실행 예시
DECLARE
    v_result NUMBER;
    v_expected NUMBER := 1000;
BEGIN
    -- 함수 실행
    v_result := FN_CALC_TAX(10000);

    -- 결과 검증
    IF v_result = v_expected THEN
        DBMS_OUTPUT.PUT_LINE('✅ 테스트 성공');
    ELSE
        DBMS_OUTPUT.PUT_LINE('❌ 테스트 실패: 예상=' || v_expected || 
                             ', 실제=' || v_result);
    END IF;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디버깅을 위한 로깅&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 로그 테이블 생성
CREATE TABLE function_debug_log (
    log_id NUMBER PRIMARY KEY,
    function_name VARCHAR2(100),
    log_level VARCHAR2(20),
    log_message CLOB,
    log_date TIMESTAMP DEFAULT SYSTIMESTAMP
);

CREATE SEQUENCE seq_debug_log START WITH 1;

-- 로깅이 포함된 함수 예시
CREATE OR REPLACE FUNCTION calculate_with_log(
    p_amount IN NUMBER
) RETURN NUMBER
IS
    v_result NUMBER;
    v_debug_enabled BOOLEAN := TRUE;  -- 개발 시에만 TRUE
BEGIN
    IF v_debug_enabled THEN
        INSERT INTO function_debug_log VALUES (
            seq_debug_log.NEXTVAL,
            'CALCULATE_WITH_LOG',
            'INFO',
            'Input: ' || p_amount,
            SYSTIMESTAMP
        );
    END IF;

    -- 비즈니스 로직
    v_result := p_amount * 1.1;

    IF v_debug_enabled THEN
        INSERT INTO function_debug_log VALUES (
            seq_debug_log.NEXTVAL,
            'CALCULATE_WITH_LOG',
            'INFO',
            'Output: ' || v_result,
            SYSTIMESTAMP
        );
        COMMIT;  -- 로그는 별도 커밋
    END IF;

    RETURN v_result;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;12. 실무 패턴 모음&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패턴 1: Safe Division (0으로 나누기 방지)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION safe_divide(
    p_numerator IN NUMBER,
    p_denominator IN NUMBER,
    p_default IN NUMBER DEFAULT NULL
) RETURN NUMBER
DETERMINISTIC
IS
BEGIN
    IF p_denominator = 0 OR p_denominator IS NULL THEN
        RETURN p_default;
    END IF;

    RETURN p_numerator / p_denominator;
END;
/

-- 사용 예
SELECT 
    product_id,
    safe_divide(total_sales, total_orders, 0) AS avg_order_value
FROM products;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패턴 2: String Aggregation&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION get_employee_skills(
    p_emp_id IN NUMBER
) RETURN VARCHAR2
IS
    v_skills VARCHAR2(4000);
BEGIN
    SELECT LISTAGG(skill_name, ', ') WITHIN GROUP (ORDER BY skill_name)
    INTO v_skills
    FROM employee_skills
    WHERE employee_id = p_emp_id;

    RETURN v_skills;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패턴 3: Date Range Validation&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION is_date_in_range(
    p_check_date IN DATE,
    p_start_date IN DATE,
    p_end_date IN DATE
) RETURN VARCHAR2
DETERMINISTIC
IS
BEGIN
    IF p_check_date BETWEEN p_start_date AND p_end_date THEN
        RETURN 'Y';
    ELSE
        RETURN 'N';
    END IF;
EXCEPTION
    WHEN OTHERS THEN
        RETURN 'N';
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패턴 4: Hierarchy Path Builder&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION get_org_path(
    p_emp_id IN NUMBER
) RETURN VARCHAR2
IS
    v_path VARCHAR2(4000);
BEGIN
    SELECT LTRIM(SYS_CONNECT_BY_PATH(department_name, ' &amp;gt; '), ' &amp;gt; ')
    INTO v_path
    FROM departments
    START WITH department_id = (
        SELECT department_id 
        FROM employees 
        WHERE employee_id = p_emp_id
    )
    CONNECT BY PRIOR parent_department_id = department_id;

    RETURN v_path;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;13. 엑셀 추출용 종합 리포트&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 함수 관리 리포트 (문서화용)
SELECT
    ROWNUM AS &quot;NO&quot;,
    o.OBJECT_NAME AS &quot;함수명&quot;,
    o.STATUS AS &quot;상태&quot;,
    TO_CHAR(o.CREATED, 'YYYY-MM-DD') AS &quot;생성일&quot;,
    TO_CHAR(o.LAST_DDL_TIME, 'YYYY-MM-DD') AS &quot;수정일&quot;,
    (SELECT COUNT(*) 
     FROM USER_SOURCE s 
     WHERE s.NAME = o.OBJECT_NAME 
       AND s.TYPE = 'FUNCTION') AS &quot;코드라인수&quot;,
    (SELECT COUNT(*) 
     FROM USER_DEPENDENCIES d 
     WHERE d.NAME = o.OBJECT_NAME 
       AND d.TYPE = 'FUNCTION') AS &quot;의존성수&quot;,
    CASE 
        WHEN EXISTS (
            SELECT 1 FROM USER_SOURCE s 
            WHERE s.NAME = o.OBJECT_NAME 
              AND UPPER(s.TEXT) LIKE '%DETERMINISTIC%'
        ) THEN 'Y' 
        ELSE 'N' 
    END AS &quot;DETERMINISTIC&quot;,
    CASE 
        WHEN EXISTS (
            SELECT 1 FROM USER_SOURCE s 
            WHERE s.NAME = o.OBJECT_NAME 
              AND UPPER(s.TEXT) LIKE '%RESULT_CACHE%'
        ) THEN 'Y' 
        ELSE 'N' 
    END AS &quot;RESULT_CACHE&quot;
FROM USER_OBJECTS o
WHERE o.OBJECT_TYPE = 'FUNCTION'
ORDER BY o.OBJECT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;14. 마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 함수는 SQL과 PL/SQL의 강력한 결합을 가능하게 하는 핵심 기능입니다. 이 가이드의 내용을 활용하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ 함수를 체계적으로 관리하고 모니터링할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 성능 최적화를 위한 명확한 지침을 얻을 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 함수와 프로시저 중 적절한 선택을 할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 의존성 관리를 통해 시스템 안정성을 높일 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 권장사항:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;함수는 순수하게 (사이드 이펙트 없이) 작성&lt;/li&gt;
&lt;li&gt;DETERMINISTIC, RESULT_CACHE를 적절히 활용&lt;/li&gt;
&lt;li&gt;SQL 내 함수 호출을 최소화 (JOIN 우선 고려)&lt;/li&gt;
&lt;li&gt;정기적인 성능 모니터링과 리팩토링&lt;/li&gt;
&lt;li&gt;명확한 명명 규칙과 문서화&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Function</category>
      <category>Oracle</category>
      <category>데이터베이스</category>
      <category>성능최적화</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/149</guid>
      <comments>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%95%A8%EC%88%98Function-%EA%B4%80%EB%A6%AC-Best-Practice-%EC%8B%A4%EB%AC%B4-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry149comment</comments>
      <pubDate>Tue, 23 Dec 2025 20:31:46 +0900</pubDate>
    </item>
    <item>
      <title>오라클 트리거(Trigger) 관리 완벽 가이드 - 실무에서 바로 쓰는 핵심 쿼리</title>
      <link>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%8A%B8%EB%A6%AC%EA%B1%B0Trigger-%EA%B4%80%EB%A6%AC-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%B0%94%EB%A1%9C-%EC%93%B0%EB%8A%94-%ED%95%B5%EC%8B%AC-%EC%BF%BC%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 트리거(Trigger)는 테이블에 특정 이벤트가 발생할 때 자동으로 실행되는 PL/SQL 블록입니다. 데이터 무결성 유지, 감사 로그 기록, 자동 계산 등에 활용되며, 실무에서는 트리거의 상태 관리와 모니터링이 매우 중요합니다. 이 가이드에서는 트리거 관리에 필요한 모든 쿼리를 목적별로 정리했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;목차&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;#1-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EA%B8%B0%EB%B3%B8-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C&quot;&gt;트리거 기본 정보 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%83%81%EC%84%B8-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C&quot;&gt;트리거 상세 정보 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%86%8C%EC%8A%A4-%EC%BD%94%EB%93%9C-%EC%A1%B0%ED%9A%8C&quot;&gt;트리거 소스 코드 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-%ED%85%8C%EC%9D%B4%EB%B8%94%EB%B3%84-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EB%AA%A9%EB%A1%9D-%EC%A1%B0%ED%9A%8C&quot;&gt;테이블별 트리거 목록 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94%EB%90%9C-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%A1%B0%ED%9A%8C&quot;&gt;비활성화된 트리거 조회&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-invalid-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%A1%B0%ED%9A%8C-%EB%B0%8F-%EC%98%A4%EB%A5%98-%ED%99%95%EC%9D%B8&quot;&gt;INVALID 트리거 조회 및 오류 확인&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%ED%99%9C%EC%84%B1%ED%99%94%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94&quot;&gt;트리거 활성화/비활성화&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-%ED%8A%B8%EB%A6%AC%EA%B1%B0-%EC%8B%A4%ED%96%89-%EC%88%9C%EC%84%9C-%ED%99%95%EC%9D%B8&quot;&gt;트리거 실행 순서 확인&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 트리거 기본 정보 조회&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리거의 존재 여부와 기본 상태를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    TRIGGER_NAME,
    TABLE_NAME,
    TRIGGER_TYPE,
    TRIGGERING_EVENT,
    STATUS,
    OWNER
FROM ALL_TRIGGERS
WHERE TRIGGER_NAME = UPPER('트리거명');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 컬럼 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;TRIGGER_TYPE&lt;/code&gt;: BEFORE/AFTER, STATEMENT/ROW 레벨&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TRIGGERING_EVENT&lt;/code&gt;: INSERT, UPDATE, DELETE 등&lt;/li&gt;
&lt;li&gt;&lt;code&gt;STATUS&lt;/code&gt;: ENABLED(활성) 또는 DISABLED(비활성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;본인 스키마만 조회:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- USER_TRIGGERS 사용 (더 빠름)
SELECT
    TRIGGER_NAME,
    TABLE_NAME,
    TRIGGER_TYPE,
    TRIGGERING_EVENT,
    STATUS
FROM USER_TRIGGERS
WHERE TRIGGER_NAME = UPPER('트리거명');&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 트리거 상세 정보 조회&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리거의 모든 속성을 한눈에 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    TRIGGER_NAME AS &quot;트리거명&quot;,
    TABLE_NAME AS &quot;테이블명&quot;,
    TRIGGER_TYPE AS &quot;타입&quot;,
    TRIGGERING_EVENT AS &quot;이벤트&quot;,
    STATUS AS &quot;상태&quot;,
    WHEN_CLAUSE AS &quot;조건&quot;,
    DESCRIPTION AS &quot;설명&quot;,
    ACTION_TYPE AS &quot;액션타입&quot;,
    TRIGGER_BODY AS &quot;본문&quot;
FROM USER_TRIGGERS
WHERE TRIGGER_NAME = UPPER('트리거명');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TRIGGER_TYPE 종류:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;BEFORE STATEMENT&lt;/code&gt;: 문장 실행 전 (1회)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BEFORE EACH ROW&lt;/code&gt;: 각 행 처리 전&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AFTER STATEMENT&lt;/code&gt;: 문장 실행 후 (1회)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AFTER EACH ROW&lt;/code&gt;: 각 행 처리 후&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TRIGGERING_EVENT 조합 예시:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UPDATE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT OR UPDATE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT OR UPDATE OR DELETE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 트리거 소스 코드 조회&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리거의 전체 로직을 확인하고 분석합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 방법 1: TRIGGER_BODY 컬럼 사용
SELECT TRIGGER_BODY
FROM USER_TRIGGERS
WHERE TRIGGER_NAME = UPPER('트리거명');&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 방법 2: ALL_SOURCE 뷰 사용 (라인별 조회)
SELECT
    LINE,
    TEXT
FROM ALL_SOURCE
WHERE NAME = UPPER('트리거명')
  AND TYPE = 'TRIGGER'
ORDER BY LINE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁:&lt;/b&gt; 복잡한 트리거는 ALL_SOURCE를 사용하면 라인별로 분석하기 편합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 테이블별 트리거 목록 조회&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 테이블에 걸려있는 모든 트리거를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    TRIGGER_NAME AS &quot;트리거명&quot;,
    TRIGGER_TYPE AS &quot;타입&quot;,
    TRIGGERING_EVENT AS &quot;이벤트&quot;,
    STATUS AS &quot;상태&quot;,
    DESCRIPTION AS &quot;설명&quot;
FROM USER_TRIGGERS
WHERE TABLE_NAME = UPPER('테이블명')
ORDER BY TRIGGER_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 활용:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 구조 변경 전 영향도 분석&lt;/li&gt;
&lt;li&gt;데이터 입력/수정 시 자동 실행되는 로직 파악&lt;/li&gt;
&lt;li&gt;성능 이슈 발생 시 트리거 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 비활성화된 트리거 조회&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 비활성 상태인 트리거를 찾습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    TRIGGER_NAME AS &quot;트리거명&quot;,
    TABLE_NAME AS &quot;테이블명&quot;,
    TRIGGERING_EVENT AS &quot;이벤트&quot;,
    STATUS AS &quot;상태&quot;
FROM USER_TRIGGERS
WHERE STATUS = 'DISABLED'
ORDER BY TABLE_NAME, TRIGGER_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확인이 필요한 경우:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포 후 의도치 않게 비활성화된 트리거 확인&lt;/li&gt;
&lt;li&gt;성능 테스트를 위해 일시적으로 비활성화한 트리거 재확인&lt;/li&gt;
&lt;li&gt;운영 중 장애 발생 시 트리거 상태 점검&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. INVALID 트리거 조회 및 오류 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일 오류가 발생한 트리거를 찾고 원인을 파악합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- INVALID 트리거 목록
SELECT
    OBJECT_NAME AS &quot;트리거명&quot;,
    STATUS AS &quot;상태&quot;,
    LAST_DDL_TIME AS &quot;최종수정일&quot;
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'TRIGGER'
  AND STATUS = 'INVALID'
ORDER BY LAST_DDL_TIME DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 컴파일 오류 상세 확인
SELECT
    LINE,
    POSITION,
    TEXT AS &quot;오류내용&quot;
FROM USER_ERRORS
WHERE NAME = UPPER('트리거명')
  AND TYPE = 'TRIGGER'
ORDER BY SEQUENCE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 INVALID 원인:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;참조하는 테이블 컬럼 변경/삭제&lt;/li&gt;
&lt;li&gt;사용 중인 함수/프로시저 변경&lt;/li&gt;
&lt;li&gt;권한 문제&lt;/li&gt;
&lt;li&gt;문법 오류&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 트리거 활성화/비활성화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리거를 켜고 끄는 DDL 명령어입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개별 트리거 제어&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트리거 비활성화
ALTER TRIGGER 트리거명 DISABLE;

-- 트리거 활성화
ALTER TRIGGER 트리거명 ENABLE;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블의 모든 트리거 제어&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 테이블의 모든 트리거 비활성화
ALTER TABLE 테이블명 DISABLE ALL TRIGGERS;

-- 테이블의 모든 트리거 활성화
ALTER TABLE 테이블명 ENABLE ALL TRIGGERS;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스키마의 모든 트리거 제어 (스크립트)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 모든 트리거 비활성화 스크립트 생성
SELECT 'ALTER TRIGGER ' || TRIGGER_NAME || ' DISABLE;'
FROM USER_TRIGGERS
WHERE STATUS = 'ENABLED';

-- 모든 트리거 활성화 스크립트 생성
SELECT 'ALTER TRIGGER ' || TRIGGER_NAME || ' ENABLE;'
FROM USER_TRIGGERS
WHERE STATUS = 'DISABLED';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 활용 시나리오:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 데이터 입력 시 성능 향상을 위해 임시 비활성화&lt;/li&gt;
&lt;li&gt;데이터 마이그레이션 작업 시&lt;/li&gt;
&lt;li&gt;테스트 환경에서 특정 로직 제외&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 트리거 실행 순서 확인&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일 이벤트에 여러 트리거가 있을 때 실행 순서를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    TRIGGER_NAME,
    TABLE_NAME,
    TRIGGER_TYPE,
    TRIGGERING_EVENT,
    STATUS,
    -- Oracle 11g 이상에서 순서 확인 가능
    ACTION_ORDER
FROM USER_TRIGGERS
WHERE TABLE_NAME = UPPER('테이블명')
  AND TRIGGERING_EVENT LIKE '%INSERT%'
ORDER BY ACTION_ORDER NULLS LAST, TRIGGER_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Oracle 11g 이상: FOLLOWS 절로 순서 제어&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE TRIGGER trg_second
AFTER INSERT ON employees
FOLLOWS trg_first  -- trg_first 다음에 실행
FOR EACH ROW
BEGIN
    -- 트리거 로직
END;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무 활용 시나리오별 쿼리 조합&lt;/b&gt;&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;사용 쿼리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;트리거 존재 여부 확인&lt;/td&gt;
&lt;td&gt;USER_TRIGGERS 기본 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테이블 변경 전 영향도 분석&lt;/td&gt;
&lt;td&gt;테이블별 트리거 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능 이슈 원인 파악&lt;/td&gt;
&lt;td&gt;테이블별 트리거 + 소스 코드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포 후 상태 점검&lt;/td&gt;
&lt;td&gt;INVALID 트리거 + 비활성화 트리거&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대량 데이터 작업 전&lt;/td&gt;
&lt;td&gt;트리거 비활성화 스크립트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 종합 관리 쿼리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전체 트리거 현황 (대시보드용)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    ROWNUM AS NO,
    TRIGGER_NAME AS &quot;트리거명&quot;,
    TABLE_NAME AS &quot;테이블명&quot;,
    TRIGGER_TYPE AS &quot;타입&quot;,
    TRIGGERING_EVENT AS &quot;이벤트&quot;,
    STATUS AS &quot;상태&quot;,
    CASE 
        WHEN STATUS = 'ENABLED' THEN '정상'
        ELSE '비활성'
    END AS &quot;비고&quot;
FROM USER_TRIGGERS
ORDER BY TABLE_NAME, TRIGGER_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블별 트리거 통계&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    TABLE_NAME AS &quot;테이블명&quot;,
    COUNT(*) AS &quot;트리거수&quot;,
    SUM(CASE WHEN STATUS = 'ENABLED' THEN 1 ELSE 0 END) AS &quot;활성&quot;,
    SUM(CASE WHEN STATUS = 'DISABLED' THEN 1 ELSE 0 END) AS &quot;비활성&quot;
FROM USER_TRIGGERS
GROUP BY TABLE_NAME
ORDER BY COUNT(*) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트별 트리거 통계&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    TRIGGERING_EVENT AS &quot;이벤트&quot;,
    COUNT(*) AS &quot;트리거수&quot;,
    SUM(CASE WHEN STATUS = 'ENABLED' THEN 1 ELSE 0 END) AS &quot;활성&quot;
FROM USER_TRIGGERS
GROUP BY TRIGGERING_EVENT
ORDER BY COUNT(*) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 성능 모니터링&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트리거 실행 통계 (V$SQL 활용)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    s.sql_text,
    s.executions AS &quot;실행횟수&quot;,
    ROUND(s.elapsed_time/1000000, 2) AS &quot;총소요시간(초)&quot;,
    ROUND(s.elapsed_time/s.executions/1000000, 4) AS &quot;평균소요시간(초)&quot;
FROM v$sql s
WHERE UPPER(s.sql_text) LIKE '%트리거명%'
ORDER BY s.executions DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;느린 트리거 찾기&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 실행 시간이 긴 트리거 식별
SELECT
    t.trigger_name,
    t.table_name,
    t.status,
    LENGTH(t.trigger_body) AS &quot;코드길이&quot;
FROM user_triggers t
WHERE t.status = 'ENABLED'
ORDER BY LENGTH(t.trigger_body) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 의존성 조회&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리거가 참조하는 객체들을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
    NAME AS &quot;트리거명&quot;,
    TYPE AS &quot;타입&quot;,
    REFERENCED_OWNER AS &quot;참조스키마&quot;,
    REFERENCED_NAME AS &quot;참조객체&quot;,
    REFERENCED_TYPE AS &quot;참조타입&quot;
FROM USER_DEPENDENCIES
WHERE NAME = UPPER('트리거명')
  AND TYPE = 'TRIGGER'
ORDER BY REFERENCED_TYPE, REFERENCED_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 구조 변경 시 영향받는 트리거 파악&lt;/li&gt;
&lt;li&gt;프로시저/함수 변경 시 관련 트리거 확인&lt;/li&gt;
&lt;li&gt;리팩토링 범위 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 재컴파일&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INVALID 상태의 트리거를 재컴파일합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 개별 트리거 재컴파일
ALTER TRIGGER 트리거명 COMPILE;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 전체 INVALID 트리거 재컴파일 스크립트 생성
SELECT 'ALTER TRIGGER ' || OBJECT_NAME || ' COMPILE;'
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'TRIGGER'
  AND STATUS = 'INVALID';&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 백업 및 복원&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DDL 추출 (백업용)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- DBMS_METADATA 패키지 사용
SELECT DBMS_METADATA.GET_DDL('TRIGGER', '트리거명') AS ddl
FROM DUAL;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 전체 트리거 DDL 추출
SELECT DBMS_METADATA.GET_DDL('TRIGGER', TRIGGER_NAME) AS ddl
FROM USER_TRIGGERS
WHERE STATUS = 'ENABLED';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁:&lt;/b&gt; 결과를 파일로 저장하여 버전 관리 시스템에 보관하세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 작성 Best Practices&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 명명 규칙&lt;/h3&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;-- 권장 명명 규칙
-- TRG_[테이블명]_[이벤트]_[타이밍]
-- 예: TRG_EMPLOYEES_INSERT_BEFORE
-- 예: TRG_ORDERS_UPDATE_AFTER&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 성능을 고려한 트리거 작성&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE OR REPLACE TRIGGER trg_example
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
    -- ❌ 나쁜 예: 트리거 내에서 SELECT 사용
    -- SELECT COUNT(*) INTO v_count FROM departments;

    -- ✅ 좋은 예: 필요한 경우만 최소한의 쿼리
    IF :NEW.department_id IS NOT NULL THEN
        :NEW.updated_date := SYSDATE;
    END IF;
END;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 순환 참조 방지&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- ❌ 위험: A 테이블 트리거가 B 테이블 수정 &amp;rarr; B 테이블 트리거가 A 테이블 수정
-- 순환 참조로 인한 무한 루프 발생 가능

-- ✅ 안전: 플래그 변수로 제어
CREATE OR REPLACE TRIGGER trg_safe
BEFORE UPDATE ON table_a
FOR EACH ROW
DECLARE
    v_trigger_active BOOLEAN := FALSE;
BEGIN
    IF NOT v_trigger_active THEN
        v_trigger_active := TRUE;
        -- 로직 수행
        v_trigger_active := FALSE;
    END IF;
END;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;트리거 디버깅&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트리거 로그 테이블 생성&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE trigger_log (
    log_id NUMBER PRIMARY KEY,
    trigger_name VARCHAR2(100),
    table_name VARCHAR2(100),
    event_type VARCHAR2(50),
    old_value VARCHAR2(4000),
    new_value VARCHAR2(4000),
    log_date DATE DEFAULT SYSDATE,
    username VARCHAR2(50) DEFAULT USER
);

CREATE SEQUENCE seq_trigger_log START WITH 1;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트리거 내 로깅 예시&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;CREATE OR REPLACE TRIGGER trg_employee_audit
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    INSERT INTO trigger_log (
        log_id, trigger_name, table_name, event_type,
        old_value, new_value
    ) VALUES (
        seq_trigger_log.NEXTVAL,
        'TRG_EMPLOYEE_AUDIT',
        'EMPLOYEES',
        'UPDATE',
        :OLD.salary,
        :NEW.salary
    );
END;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;엑셀 추출용 쿼리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서화나 보고서 작성을 위한 쿼리입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    ROWNUM AS &quot;NO&quot;,
    TRIGGER_NAME AS &quot;트리거명&quot;,
    TABLE_NAME AS &quot;테이블명&quot;,
    CASE 
        WHEN TRIGGER_TYPE LIKE 'BEFORE%' THEN 'BEFORE'
        ELSE 'AFTER'
    END AS &quot;실행시점&quot;,
    CASE 
        WHEN TRIGGER_TYPE LIKE '%EACH ROW%' THEN 'ROW'
        ELSE 'STATEMENT'
    END AS &quot;레벨&quot;,
    TRIGGERING_EVENT AS &quot;이벤트&quot;,
    STATUS AS &quot;상태&quot;,
    SUBSTR(DESCRIPTION, 1, 100) AS &quot;설명&quot;
FROM USER_TRIGGERS
ORDER BY TABLE_NAME, TRIGGER_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;문제 해결 가이드&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q1. 트리거가 실행되지 않아요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;체크리스트:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;트리거 상태 확인 (ENABLED/DISABLED)&lt;/li&gt;
&lt;li&gt;트리거 조건(WHEN 절) 확인&lt;/li&gt;
&lt;li&gt;트리거 이벤트 타입 확인 (INSERT/UPDATE/DELETE)&lt;/li&gt;
&lt;li&gt;권한 문제 확인&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 상태 및 조건 확인
SELECT 
    trigger_name, 
    status, 
    when_clause,
    triggering_event
FROM user_triggers
WHERE trigger_name = 'YOUR_TRIGGER';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q2. 트리거로 인해 성능이 느려요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 테이블의 트리거 개수 확인
SELECT table_name, COUNT(*) AS trigger_count
FROM user_triggers
WHERE status = 'ENABLED'
GROUP BY table_name
HAVING COUNT(*) &amp;gt; 3
ORDER BY COUNT(*) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결방안:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 트리거 제거&lt;/li&gt;
&lt;li&gt;ROW 레벨 &amp;rarr; STATEMENT 레벨 변경 검토&lt;/li&gt;
&lt;li&gt;트리거 내 복잡한 쿼리 최적화&lt;/li&gt;
&lt;li&gt;트리거 로직을 애플리케이션으로 이전 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Q3. 트리거 순환 참조 오류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오류 메시지:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;ORA-04091: table X is mutating, trigger/function may not see it&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결방법:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AUTONOMOUS_TRANSACTION 사용&lt;/li&gt;
&lt;li&gt;Compound Trigger 사용 (11g 이상)&lt;/li&gt;
&lt;li&gt;패키지 변수를 활용한 제어&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마치며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 트리거는 강력한 기능이지만, 잘못 사용하면 성능 저하와 유지보수의 어려움을 초래할 수 있습니다. 이 가이드의 쿼리들을 활용하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ 트리거 상태를 체계적으로 관리할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 성능 문제를 빠르게 진단할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 배포 전후 트리거 상태를 정확히 점검할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 트리거 의존성을 명확히 파악할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트리거 사용 권장사항:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;꼭 필요한 경우에만 사용&lt;/li&gt;
&lt;li&gt;트리거 내 복잡한 로직은 프로시저로 분리&lt;/li&gt;
&lt;li&gt;정기적인 성능 모니터링&lt;/li&gt;
&lt;li&gt;명확한 명명 규칙과 문서화&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Oracle</category>
      <category>Trigger</category>
      <category>데이터베이스</category>
      <category>오라클</category>
      <category>트리거</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/148</guid>
      <comments>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%8A%B8%EB%A6%AC%EA%B1%B0Trigger-%EA%B4%80%EB%A6%AC-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%B0%94%EB%A1%9C-%EC%93%B0%EB%8A%94-%ED%95%B5%EC%8B%AC-%EC%BF%BC%EB%A6%AC#entry148comment</comments>
      <pubDate>Mon, 22 Dec 2025 22:27:32 +0900</pubDate>
    </item>
    <item>
      <title>오라클 프로시저 정보 조회 완전 가이드 - 실무에서 바로 쓰는 7가지 쿼리</title>
      <link>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%B0%94%EB%A1%9C-%EC%93%B0%EB%8A%94-7%EA%B0%80%EC%A7%80-%EC%BF%BC%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 데이터베이스에서 프로시저(Stored Procedure) 정보를 조회할 때, 데이터 딕셔너리 뷰를 활용하면 정확하고 체계적으로 필요한 정보를 얻을 수 있습니다. 이 글에서는 실무에서 자주 사용하는 7가지 핵심 쿼리를 목적별로 정리했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로시저 기본 정보 조회 (필수)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 확인해야 할 프로시저의 기본 정보입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    OWNER,
    OBJECT_NAME,
    OBJECT_TYPE,
    STATUS,
    CREATED,
    LAST_DDL_TIME
FROM ALL_OBJECTS
WHERE OBJECT_TYPE = 'PROCEDURE'
  AND OBJECT_NAME = UPPER('프로시저명');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 컬럼 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;OWNER&lt;/code&gt;: 프로시저 소유자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;STATUS&lt;/code&gt;: VALID(정상) 또는 INVALID(오류)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATED&lt;/code&gt;: 최초 생성일&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LAST_DDL_TIME&lt;/code&gt;: 마지막 수정일&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팁:&lt;/b&gt; 본인 스키마의 프로시저만 조회하려면 &lt;code&gt;ALL_OBJECTS&lt;/code&gt; 대신 &lt;code&gt;USER_OBJECTS&lt;/code&gt;를 사용하세요.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프로시저 파라미터 정보 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저의 입력/출력 파라미터 구조를 파악할 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    ARGUMENT_NAME,
    POSITION,
    IN_OUT,
    DATA_TYPE,
    DATA_LENGTH,
    DATA_PRECISION,
    DATA_SCALE
FROM ALL_ARGUMENTS
WHERE OBJECT_NAME = UPPER('프로시저명')
ORDER BY POSITION;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 컬럼 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;IN_OUT&lt;/code&gt;: IN, OUT, IN OUT 구분&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSITION&lt;/code&gt;: 파라미터 순서 (0부터 시작)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ARGUMENT_NAME&lt;/code&gt;이 NULL인 경우: RETURN 값 또는 첫 번째 파라미터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패키지 내 프로시저 조회 시:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;AND PACKAGE_NAME = '패키지명'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 프로시저 소스 코드 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저의 전체 로직을 확인할 때 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    LINE,
    TEXT
FROM ALL_SOURCE
WHERE NAME = UPPER('프로시저명')
  AND TYPE = 'PROCEDURE'
ORDER BY LINE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 코드가 라인별로 정렬되어 출력되므로, 로직을 단계별로 분석하기 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;권장:&lt;/b&gt; 본인 스키마만 조회할 때는 &lt;code&gt;USER_SOURCE&lt;/code&gt;를 사용하면 더 빠릅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 특정 테이블을 참조하는 프로시저 찾기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 구조 변경 전 영향도 분석에 필수적인 쿼리입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT DISTINCT
    NAME AS PROCEDURE_NAME
FROM ALL_SOURCE
WHERE TYPE = 'PROCEDURE'
  AND UPPER(TEXT) LIKE '%테이블명%';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실무 활용 예시:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 컬럼 변경 전 영향받는 프로시저 파악&lt;/li&gt;
&lt;li&gt;테이블 삭제 전 의존성 체크&lt;/li&gt;
&lt;li&gt;리팩토링 범위 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. INVALID 상태 프로시저 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴파일 오류가 발생한 프로시저를 한눈에 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    OBJECT_NAME,
    STATUS,
    LAST_DDL_TIME
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'PROCEDURE'
  AND STATUS = 'INVALID';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 후 또는 테이블 구조 변경 후 반드시 확인해야 할 쿼리입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 프로시저 컴파일 에러 상세 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INVALID 상태인 프로시저의 정확한 오류 원인을 파악합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    LINE,
    POSITION,
    TEXT
FROM USER_ERRORS
WHERE NAME = UPPER('프로시저명')
ORDER BY SEQUENCE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Eclipse, DBeaver 등 IDE의 오류 메시지와 1:1 매칭&lt;/li&gt;
&lt;li&gt;라인 번호와 포지션으로 정확한 오류 위치 파악&lt;/li&gt;
&lt;li&gt;운영 장애 발생 시 빠른 원인 분석 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 패키지 내 프로시저 목록 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지에 포함된 모든 프로시저를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
    OBJECT_NAME,
    PROCEDURE_NAME
FROM ALL_PROCEDURES
WHERE OBJECT_TYPE = 'PACKAGE'
  AND OBJECT_NAME = '패키지명';&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서 패키지 구조를 파악할 때 유용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 활용 시나리오별 쿼리 조합&lt;/h2&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;목적&lt;/th&gt;
&lt;th&gt;사용 쿼리&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;프로시저 존재 여부 확인&lt;/td&gt;
&lt;td&gt;ALL_OBJECTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;호출 방법 확인 (파라미터)&lt;/td&gt;
&lt;td&gt;ALL_ARGUMENTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;로직 분석 및 수정&lt;/td&gt;
&lt;td&gt;ALL_SOURCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;컴파일 오류 해결&lt;/td&gt;
&lt;td&gt;USER_ERRORS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테이블 변경 영향도 분석&lt;/td&gt;
&lt;td&gt;ALL_SOURCE (LIKE)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추가 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 권한 문제 해결&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;ALL_*&lt;/code&gt; 뷰: 접근 가능한 모든 객체&lt;/li&gt;
&lt;li&gt;&lt;code&gt;USER_*&lt;/code&gt; 뷰: 본인 스키마만 (더 빠름)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DBA_*&lt;/code&gt; 뷰: DBA 권한 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 성능 최적화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OBJECT_NAME 조건에 항상 &lt;code&gt;UPPER()&lt;/code&gt; 함수 사용&lt;/li&gt;
&lt;li&gt;본인 스키마만 조회할 때는 &lt;code&gt;USER_*&lt;/code&gt; 뷰 우선 사용&lt;/li&gt;
&lt;li&gt;LIKE 검색은 필요한 경우에만 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 자동화 스크립트&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 전체 INVALID 프로시저 재컴파일
SELECT 'ALTER PROCEDURE ' || OBJECT_NAME || ' COMPILE;'
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'PROCEDURE'
  AND STATUS = 'INVALID';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보너스: 통합 조회 쿼리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저의 모든 정보를 한 번에 확인하고 싶을 때 사용하는 통합 쿼리입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 프로시저 상세 정보 (기본 정보 + 파라미터)
SELECT 
    o.OBJECT_NAME,
    o.STATUS,
    o.CREATED,
    o.LAST_DDL_TIME,
    a.ARGUMENT_NAME,
    a.POSITION,
    a.IN_OUT,
    a.DATA_TYPE
FROM USER_OBJECTS o
LEFT JOIN ALL_ARGUMENTS a 
    ON o.OBJECT_NAME = a.OBJECT_NAME
WHERE o.OBJECT_TYPE = 'PROCEDURE'
  AND o.OBJECT_NAME = UPPER('프로시저명')
ORDER BY a.POSITION;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엑셀 추출용 쿼리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서화나 보고서 작성을 위해 엑셀로 추출하기 좋은 형태의 쿼리입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 전체 프로시저 목록 (엑셀 추출용)
SELECT 
    ROWNUM AS NO,
    OBJECT_NAME AS &quot;프로시저명&quot;,
    STATUS AS &quot;상태&quot;,
    TO_CHAR(CREATED, 'YYYY-MM-DD') AS &quot;생성일&quot;,
    TO_CHAR(LAST_DDL_TIME, 'YYYY-MM-DD HH24:MI:SS') AS &quot;최종수정일&quot;,
    CASE 
        WHEN STATUS = 'VALID' THEN '정상'
        ELSE '오류'
    END AS &quot;비고&quot;
FROM USER_OBJECTS
WHERE OBJECT_TYPE = 'PROCEDURE'
ORDER BY OBJECT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특정 스키마의 모든 프로시저 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 스키마의 프로시저를 조회할 때 (권한 필요):&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT
    OWNER AS &quot;스키마&quot;,
    OBJECT_NAME AS &quot;프로시저명&quot;,
    STATUS AS &quot;상태&quot;,
    CREATED AS &quot;생성일&quot;
FROM ALL_OBJECTS
WHERE OBJECT_TYPE = 'PROCEDURE'
  AND OWNER = 'HR'  -- 특정 스키마명
ORDER BY OBJECT_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프로시저 의존성 조회&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저가 참조하는 객체들을 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
    NAME AS &quot;프로시저명&quot;,
    TYPE AS &quot;타입&quot;,
    REFERENCED_OWNER AS &quot;참조스키마&quot;,
    REFERENCED_NAME AS &quot;참조객체&quot;,
    REFERENCED_TYPE AS &quot;참조타입&quot;
FROM USER_DEPENDENCIES
WHERE NAME = UPPER('프로시저명')
  AND TYPE = 'PROCEDURE'
ORDER BY REFERENCED_TYPE, REFERENCED_NAME;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 통계 조회 (성능 분석)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로시저의 실행 통계를 확인합니다 (10g 이상):&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    object_name,
    executions AS &quot;실행횟수&quot;,
    ROUND(elapsed_time/1000000, 2) AS &quot;총소요시간(초)&quot;,
    ROUND(elapsed_time/executions/1000000, 4) AS &quot;평균소요시간(초)&quot;
FROM v$sql
WHERE sql_text LIKE '%프로시저명%'
  AND command_type = 47  -- PL/SQL EXECUTE
ORDER BY executions DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클의 데이터 딕셔너리 뷰는 프로시저 관리에 있어 강력한 도구입니다. 이 가이드의 쿼리들을 활용하면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;✅ 프로시저 정보를 빠르게 파악할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 컴파일 오류를 신속하게 해결할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 테이블 변경의 영향도를 정확히 분석할 수 있습니다&lt;/li&gt;
&lt;li&gt;✅ 성능 문제를 체계적으로 진단할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일상적인 개발과 운영 업무에서 이 가이드를 참고하여 효율적으로 작업하시기 바랍니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;태그:&lt;/b&gt; &lt;code&gt;Oracle&lt;/code&gt; &lt;code&gt;Stored Procedure&lt;/code&gt; &lt;code&gt;PL/SQL&lt;/code&gt; &lt;code&gt;Database&lt;/code&gt; &lt;code&gt;데이터베이스&lt;/code&gt; &lt;code&gt;프로시저&lt;/code&gt; &lt;code&gt;오라클&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;난이도:&lt;/b&gt; 초급~중급&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예상 소요 시간:&lt;/b&gt; 15분&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관련 글 추천&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오라클 함수(Function) 정보 조회 가이드&lt;/li&gt;
&lt;li&gt;오라클 트리거(Trigger) 관리 완벽 가이드&lt;/li&gt;
&lt;li&gt;오라클 패키지(Package) 개발 Best Practice&lt;/li&gt;
&lt;li&gt;데이터 딕셔너리 뷰 활용 완전 정복&lt;/li&gt;
&lt;li&gt;PL/SQL 성능 튜닝 실전 가이드&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DB/Oracle</category>
      <category>Oracle</category>
      <category>Stored Procedure</category>
      <category>오라클</category>
      <category>프로시저</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/147</guid>
      <comments>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EB%B0%94%EB%A1%9C-%EC%93%B0%EB%8A%94-7%EA%B0%80%EC%A7%80-%EC%BF%BC%EB%A6%AC#entry147comment</comments>
      <pubDate>Mon, 22 Dec 2025 22:14:21 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #7장. Spring과의 통합</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-7%EC%9E%A5-Spring%EA%B3%BC%EC%9D%98-%ED%86%B5%ED%95%A9</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dczzaz/btsP4uEkyDh/5n4j75UnLC30B5ZgRaLEs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dczzaz/btsP4uEkyDh/5n4j75UnLC30B5ZgRaLEs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dczzaz/btsP4uEkyDh/5n4j75UnLC30B5ZgRaLEs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdczzaz%2FbtsP4uEkyDh%2F5n4j75UnLC30B5ZgRaLEs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7장.&amp;nbsp;Spring과의&amp;nbsp;통합&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 Spring MVC와 Thymeleaf 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf는 Spring Framework와의 완벽한 통합을 위해 설계되었습니다. Spring MVC의 Model-View-Controller 패턴과 자연스럽게 연동되며, Spring의 다양한 기능을 템플릿에서 직접 활용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 Spring MVC 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// src/main/java/com/example/config/WebConfig.java
@Configuration
@EnableWebMvc
@ComponentScan(&quot;com.example.controller&quot;)
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix(&quot;classpath:/templates/&quot;);
        templateResolver.setSuffix(&quot;.html&quot;);
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCharacterEncoding(&quot;UTF-8&quot;);
        templateResolver.setCacheable(false); // 개발 환경에서만
        return templateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.setEnableSpringELCompiler(true);
        return templateEngine;
    }

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setCharacterEncoding(&quot;UTF-8&quot;);
        return viewResolver;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Spring Boot 자동 설정&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot를 사용하면 설정이 훨씬 간단해집니다:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- pom.xml --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-thymeleaf&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
spring:
  thymeleaf:
    cache: false  # 개발 시에만
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML
    encoding: UTF-8&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;컨트롤러와 모델 데이터 전달&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// src/main/java/com/example/controller/HomeController.java
@Controller
public class HomeController {

    @Autowired
    private ProductService productService;

    @Autowired
    private UserService userService;

    @GetMapping(&quot;/&quot;)
    public String home(Model model, Authentication authentication) {
        // 기본 페이지 정보 설정
        model.addAttribute(&quot;pageTitle&quot;, &quot;홈&quot;);
        model.addAttribute(&quot;pageDescription&quot;, &quot;MyApp에 오신 것을 환영합니다&quot;);

        // 추천 상품 데이터
        List&amp;lt;Product&amp;gt; featuredProducts = productService.getFeaturedProducts();
        model.addAttribute(&quot;featuredProducts&quot;, featuredProducts);

        // 현재 사용자 정보 (Spring Security 연동)
        if (authentication != null &amp;amp;&amp;amp; authentication.isAuthenticated()) {
            String username = authentication.getName();
            User currentUser = userService.findByUsername(username);
            model.addAttribute(&quot;currentUser&quot;, currentUser);
        }

        // 사이드바 데이터
        model.addAttribute(&quot;recentPosts&quot;, productService.getRecentPosts(5));
        model.addAttribute(&quot;categories&quot;, productService.getAllCategories());

        return &quot;home&quot;; // templates/home.html
    }

    @GetMapping(&quot;/products&quot;)
    public String products(
            @RequestParam(required = false) String category,
            @RequestParam(defaultValue = &quot;0&quot;) int page,
            @RequestParam(defaultValue = &quot;12&quot;) int size,
            @RequestParam(required = false) String sort,
            Model model) {

        // 페이지네이션과 필터링
        Pageable pageable = PageRequest.of(page, size, getSortOrder(sort));
        Page&amp;lt;Product&amp;gt; productPage;

        if (category != null &amp;amp;&amp;amp; !category.isEmpty()) {
            productPage = productService.findByCategory(category, pageable);
        } else {
            productPage = productService.findAll(pageable);
        }

        // 모델에 데이터 추가
        model.addAttribute(&quot;products&quot;, productPage.getContent());
        model.addAttribute(&quot;currentPage&quot;, page);
        model.addAttribute(&quot;totalPages&quot;, productPage.getTotalPages());
        model.addAttribute(&quot;totalProducts&quot;, productPage.getTotalElements());
        model.addAttribute(&quot;selectedCategory&quot;, category);
        model.addAttribute(&quot;sort&quot;, sort);

        // 페이지 정보
        model.addAttribute(&quot;pageTitle&quot;, &quot;상품 목록&quot;);
        model.addAttribute(&quot;breadcrumbs&quot;, Arrays.asList(
            new Breadcrumb(&quot;상품&quot;, &quot;/products&quot;)
        ));

        // 필터 옵션
        model.addAttribute(&quot;categories&quot;, productService.getAllCategories());

        return &quot;products/list&quot;;
    }

    @GetMapping(&quot;/products/{id}&quot;)
    public String productDetail(@PathVariable Long id, Model model) {
        Product product = productService.findById(id)
            .orElseThrow(() -&amp;gt; new ProductNotFoundException(&quot;상품을 찾을 수 없습니다: &quot; + id));

        model.addAttribute(&quot;product&quot;, product);
        model.addAttribute(&quot;pageTitle&quot;, product.getName());
        model.addAttribute(&quot;pageDescription&quot;, product.getShortDescription());

        // 브레드크럼 설정
        model.addAttribute(&quot;breadcrumbs&quot;, Arrays.asList(
            new Breadcrumb(&quot;상품&quot;, &quot;/products&quot;),
            new Breadcrumb(product.getCategory().getName(), 
                          &quot;/products?category=&quot; + product.getCategory().getId()),
            new Breadcrumb(product.getName(), &quot;&quot;)
        ));

        // 관련 상품
        List&amp;lt;Product&amp;gt; relatedProducts = productService.getRelatedProducts(product, 4);
        model.addAttribute(&quot;relatedProducts&quot;, relatedProducts);

        // 리뷰
        List&amp;lt;Review&amp;gt; reviews = productService.getReviews(id, 10);
        model.addAttribute(&quot;reviews&quot;, reviews);

        return &quot;products/detail&quot;;
    }

    private Sort getSortOrder(String sort) {
        if (sort == null) return Sort.by(&quot;name&quot;).ascending();

        switch (sort) {
            case &quot;price-low&quot;: return Sort.by(&quot;price&quot;).ascending();
            case &quot;price-high&quot;: return Sort.by(&quot;price&quot;).descending();
            case &quot;newest&quot;: return Sort.by(&quot;createdAt&quot;).descending();
            case &quot;name&quot;: return Sort.by(&quot;name&quot;).ascending();
            default: return Sort.by(&quot;name&quot;).ascending();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;폼 처리 컨트롤러&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// src/main/java/com/example/controller/ContactController.java
@Controller
@RequestMapping(&quot;/contact&quot;)
public class ContactController {

    @Autowired
    private ContactService contactService;

    @GetMapping
    public String showContactForm(Model model) {
        model.addAttribute(&quot;contactForm&quot;, new ContactForm());
        model.addAttribute(&quot;pageTitle&quot;, &quot;문의하기&quot;);
        model.addAttribute(&quot;inquiryTypes&quot;, getInquiryTypes());

        return &quot;contact&quot;;
    }

    @PostMapping
    public String submitContact(@Valid @ModelAttribute ContactForm contactForm,
                              BindingResult bindingResult,
                              Model model,
                              RedirectAttributes redirectAttributes) {

        // 유효성 검증 실패 시
        if (bindingResult.hasErrors()) {
            model.addAttribute(&quot;pageTitle&quot;, &quot;문의하기&quot;);
            model.addAttribute(&quot;inquiryTypes&quot;, getInquiryTypes());
            return &quot;contact&quot;;
        }

        try {
            // 문의 처리
            contactService.processInquiry(contactForm);

            // 성공 메시지
            redirectAttributes.addFlashAttribute(&quot;successMessage&quot;, 
                &quot;문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.&quot;);

            return &quot;redirect:/contact&quot;;

        } catch (Exception e) {
            model.addAttribute(&quot;errorMessage&quot;, &quot;문의 처리 중 오류가 발생했습니다. 다시 시도해주세요.&quot;);
            model.addAttribute(&quot;pageTitle&quot;, &quot;문의하기&quot;);
            model.addAttribute(&quot;inquiryTypes&quot;, getInquiryTypes());
            return &quot;contact&quot;;
        }
    }

    private List&amp;lt;InquiryType&amp;gt; getInquiryTypes() {
        return Arrays.asList(
            new InquiryType(&quot;general&quot;, &quot;일반 문의&quot;),
            new InquiryType(&quot;support&quot;, &quot;기술 지원&quot;),
            new InquiryType(&quot;sales&quot;, &quot;영업 문의&quot;),
            new InquiryType(&quot;partnership&quot;, &quot;제휴 문의&quot;)
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;모델 클래스&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// src/main/java/com/example/model/ContactForm.java
public class ContactForm {

    @NotBlank(message = &quot;이름을 입력해주세요&quot;)
    @Size(min = 2, max = 50, message = &quot;이름은 2자 이상 50자 이하로 입력해주세요&quot;)
    private String name;

    @NotBlank(message = &quot;이메일을 입력해주세요&quot;)
    @Email(message = &quot;올바른 이메일 형식을 입력해주세요&quot;)
    private String email;

    @NotBlank(message = &quot;문의 유형을 선택해주세요&quot;)
    private String inquiryType;

    @NotBlank(message = &quot;메시지를 입력해주세요&quot;)
    @Size(min = 10, max = 1000, message = &quot;메시지는 10자 이상 1000자 이하로 입력해주세요&quot;)
    private String message;

    private String company; // 선택적 필드
    private String phone;   // 선택적 필드

    // 생성자, getter, setter
    public ContactForm() {}

    // getters and setters...
}

// src/main/java/com/example/model/Breadcrumb.java
public class Breadcrumb {
    private String title;
    private String url;

    public Breadcrumb(String title, String url) {
        this.title = title;
        this.url = url;
    }

    // getters and setters...
}

// src/main/java/com/example/model/InquiryType.java
public class InquiryType {
    private String value;
    private String label;

    public InquiryType(String value, String label) {
        this.value = value;
        this.label = label;
    }

    // getters and setters...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 Spring Security 통합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security와 Thymeleaf의 통합을 통해 인증과 권한 기반의 UI를 구현할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;의존성 추가&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- pom.xml --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.thymeleaf.extras&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;thymeleaf-extras-springsecurity6&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Security 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// src/main/java/com/example/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -&amp;gt; authz
                .requestMatchers(&quot;/&quot;, &quot;/home&quot;, &quot;/products/**&quot;, &quot;/css/**&quot;, &quot;/js/**&quot;, &quot;/images/**&quot;).permitAll()
                .requestMatchers(&quot;/signup&quot;, &quot;/login&quot;).permitAll()
                .requestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
                .requestMatchers(&quot;/api/**&quot;).hasAnyRole(&quot;USER&quot;, &quot;ADMIN&quot;)
                .anyRequest().authenticated()
            )
            .formLogin(form -&amp;gt; form
                .loginPage(&quot;/login&quot;)
                .loginProcessingUrl(&quot;/login&quot;)
                .defaultSuccessUrl(&quot;/dashboard&quot;, true)
                .failureUrl(&quot;/login?error&quot;)
                .usernameParameter(&quot;email&quot;)
                .passwordParameter(&quot;password&quot;)
            )
            .logout(logout -&amp;gt; logout
                .logoutUrl(&quot;/logout&quot;)
                .logoutSuccessUrl(&quot;/&quot;)
                .invalidateHttpSession(true)
                .deleteCookies(&quot;JSESSIONID&quot;)
            )
            .rememberMe(remember -&amp;gt; remember
                .key(&quot;mySecretKey&quot;)
                .tokenValiditySeconds(86400) // 1일
                .userDetailsService(userDetailsService)
            );

        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;로그인 페이지&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/auth/login.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;
      xmlns:sec=&quot;http://www.thymeleaf.org/extras/spring-security&quot;
      th:with=&quot;pageTitle='로그인'&quot;&amp;gt;

&amp;lt;head th:replace=&quot;~{layouts/base :: head}&quot;&amp;gt;&amp;lt;/head&amp;gt;

&amp;lt;body class=&quot;auth-page&quot;&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: header}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;div class=&quot;row justify-content-center&quot;&amp;gt;
            &amp;lt;div class=&quot;col-md-6 col-lg-4&quot;&amp;gt;
                &amp;lt;div class=&quot;card shadow&quot;&amp;gt;
                    &amp;lt;div class=&quot;card-body p-5&quot;&amp;gt;
                        &amp;lt;div class=&quot;text-center mb-4&quot;&amp;gt;
                            &amp;lt;h2&amp;gt;로그인&amp;lt;/h2&amp;gt;
                            &amp;lt;p class=&quot;text-muted&quot;&amp;gt;계정에 로그인하세요&amp;lt;/p&amp;gt;
                        &amp;lt;/div&amp;gt;

                        &amp;lt;!-- 오류 메시지 --&amp;gt;
                        &amp;lt;div th:if=&quot;${param.error}&quot; class=&quot;alert alert-danger&quot;&amp;gt;
                            &amp;lt;i class=&quot;fas fa-exclamation-triangle&quot;&amp;gt;&amp;lt;/i&amp;gt;
                            이메일 또는 비밀번호가 올바르지 않습니다.
                        &amp;lt;/div&amp;gt;

                        &amp;lt;!-- 로그아웃 메시지 --&amp;gt;
                        &amp;lt;div th:if=&quot;${param.logout}&quot; class=&quot;alert alert-success&quot;&amp;gt;
                            &amp;lt;i class=&quot;fas fa-check-circle&quot;&amp;gt;&amp;lt;/i&amp;gt;
                            성공적으로 로그아웃되었습니다.
                        &amp;lt;/div&amp;gt;

                        &amp;lt;form th:action=&quot;@{/login}&quot; method=&quot;post&quot;&amp;gt;
                            &amp;lt;div class=&quot;mb-3&quot;&amp;gt;
                                &amp;lt;label for=&quot;email&quot; class=&quot;form-label&quot;&amp;gt;이메일&amp;lt;/label&amp;gt;
                                &amp;lt;input type=&quot;email&quot; class=&quot;form-control&quot; id=&quot;email&quot; name=&quot;email&quot; 
                                       th:value=&quot;${param.email}&quot; required&amp;gt;
                            &amp;lt;/div&amp;gt;

                            &amp;lt;div class=&quot;mb-3&quot;&amp;gt;
                                &amp;lt;label for=&quot;password&quot; class=&quot;form-label&quot;&amp;gt;비밀번호&amp;lt;/label&amp;gt;
                                &amp;lt;input type=&quot;password&quot; class=&quot;form-control&quot; id=&quot;password&quot; name=&quot;password&quot; required&amp;gt;
                            &amp;lt;/div&amp;gt;

                            &amp;lt;div class=&quot;mb-3 form-check&quot;&amp;gt;
                                &amp;lt;input type=&quot;checkbox&quot; class=&quot;form-check-input&quot; id=&quot;remember-me&quot; name=&quot;remember-me&quot;&amp;gt;
                                &amp;lt;label class=&quot;form-check-label&quot; for=&quot;remember-me&quot;&amp;gt;
                                    로그인 상태 유지
                                &amp;lt;/label&amp;gt;
                            &amp;lt;/div&amp;gt;

                            &amp;lt;div class=&quot;d-grid&quot;&amp;gt;
                                &amp;lt;button type=&quot;submit&quot; class=&quot;btn btn-primary&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
                            &amp;lt;/div&amp;gt;

                            &amp;lt;div class=&quot;text-center mt-3&quot;&amp;gt;
                                &amp;lt;p class=&quot;mb-0&quot;&amp;gt;계정이 없으신가요? 
                                    &amp;lt;a th:href=&quot;@{/signup}&quot;&amp;gt;회원가입&amp;lt;/a&amp;gt;
                                &amp;lt;/p&amp;gt;
                                &amp;lt;a th:href=&quot;@{/forgot-password}&quot; class=&quot;text-muted&quot;&amp;gt;비밀번호를 잊으셨나요?&amp;lt;/a&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;lt;/form&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:replace=&quot;~{fragments/common :: footer}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;권한 기반 네비게이션&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/fragments/navigation.html --&amp;gt;
&amp;lt;nav th:fragment=&quot;userMenu&quot; 
     xmlns:sec=&quot;http://www.thymeleaf.org/extras/spring-security&quot;
     class=&quot;user-navigation&quot;&amp;gt;

    &amp;lt;!-- 로그인하지 않은 사용자 --&amp;gt;
    &amp;lt;div sec:authorize=&quot;!isAuthenticated()&quot; class=&quot;guest-menu&quot;&amp;gt;
        &amp;lt;a th:href=&quot;@{/login}&quot; class=&quot;btn btn-outline-primary&quot;&amp;gt;로그인&amp;lt;/a&amp;gt;
        &amp;lt;a th:href=&quot;@{/signup}&quot; class=&quot;btn btn-primary&quot;&amp;gt;회원가입&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 로그인한 사용자 --&amp;gt;
    &amp;lt;div sec:authorize=&quot;isAuthenticated()&quot; class=&quot;user-menu&quot;&amp;gt;
        &amp;lt;div class=&quot;dropdown&quot;&amp;gt;
            &amp;lt;button class=&quot;btn btn-link dropdown-toggle&quot; type=&quot;button&quot; 
                    data-bs-toggle=&quot;dropdown&quot; aria-expanded=&quot;false&quot;&amp;gt;
                &amp;lt;i class=&quot;fas fa-user&quot;&amp;gt;&amp;lt;/i&amp;gt;
                &amp;lt;span sec:authentication=&quot;name&quot;&amp;gt;사용자&amp;lt;/span&amp;gt;
            &amp;lt;/button&amp;gt;

            &amp;lt;ul class=&quot;dropdown-menu&quot;&amp;gt;
                &amp;lt;!-- 일반 사용자 메뉴 --&amp;gt;
                &amp;lt;li sec:authorize=&quot;hasRole('USER')&quot;&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/profile}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-user-circle&quot;&amp;gt;&amp;lt;/i&amp;gt; 내 프로필
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li sec:authorize=&quot;hasRole('USER')&quot;&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/orders}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-shopping-bag&quot;&amp;gt;&amp;lt;/i&amp;gt; 주문 내역
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li sec:authorize=&quot;hasRole('USER')&quot;&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/wishlist}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-heart&quot;&amp;gt;&amp;lt;/i&amp;gt; 찜 목록
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li&amp;gt;&amp;lt;hr class=&quot;dropdown-divider&quot;&amp;gt;&amp;lt;/li&amp;gt;

                &amp;lt;!-- 관리자 메뉴 --&amp;gt;
                &amp;lt;li sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/admin/dashboard}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-tachometer-alt&quot;&amp;gt;&amp;lt;/i&amp;gt; 관리자 대시보드
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/admin/users}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-users&quot;&amp;gt;&amp;lt;/i&amp;gt; 사용자 관리
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/admin/products}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-boxes&quot;&amp;gt;&amp;lt;/i&amp;gt; 상품 관리
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;&amp;lt;hr class=&quot;dropdown-divider&quot;&amp;gt;&amp;lt;/li&amp;gt;

                &amp;lt;!-- 공통 메뉴 --&amp;gt;
                &amp;lt;li&amp;gt;
                    &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{/settings}&quot;&amp;gt;
                        &amp;lt;i class=&quot;fas fa-cog&quot;&amp;gt;&amp;lt;/i&amp;gt; 설정
                    &amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;li&amp;gt;
                    &amp;lt;form th:action=&quot;@{/logout}&quot; method=&quot;post&quot; style=&quot;display: inline;&quot;&amp;gt;
                        &amp;lt;button type=&quot;submit&quot; class=&quot;dropdown-item text-danger border-0 bg-transparent&quot;&amp;gt;
                            &amp;lt;i class=&quot;fas fa-sign-out-alt&quot;&amp;gt;&amp;lt;/i&amp;gt; 로그아웃
                        &amp;lt;/button&amp;gt;
                    &amp;lt;/form&amp;gt;
                &amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/nav&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;권한 기반 콘텐츠 표시&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/dashboard.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;
      xmlns:sec=&quot;http://www.thymeleaf.org/extras/spring-security&quot;
      th:with=&quot;pageTitle='대시보드'&quot;&amp;gt;

&amp;lt;head th:replace=&quot;~{layouts/base :: head}&quot;&amp;gt;&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: header}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;container mt-4&quot;&amp;gt;
        &amp;lt;!-- 사용자 환영 메시지 --&amp;gt;
        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;div class=&quot;col-12&quot;&amp;gt;
                &amp;lt;div class=&quot;welcome-section mb-4&quot;&amp;gt;
                    &amp;lt;h1&amp;gt;안녕하세요, &amp;lt;span sec:authentication=&quot;name&quot;&amp;gt;사용자&amp;lt;/span&amp;gt;님!&amp;lt;/h1&amp;gt;
                    &amp;lt;p class=&quot;text-muted&quot;&amp;gt;
                        귀하의 권한: 
                        &amp;lt;span sec:authorize=&quot;hasRole('ADMIN')&quot; class=&quot;badge bg-danger&quot;&amp;gt;관리자&amp;lt;/span&amp;gt;
                        &amp;lt;span sec:authorize=&quot;hasRole('MODERATOR')&quot; class=&quot;badge bg-warning&quot;&amp;gt;운영자&amp;lt;/span&amp;gt;
                        &amp;lt;span sec:authorize=&quot;hasRole('USER')&quot; class=&quot;badge bg-primary&quot;&amp;gt;일반 사용자&amp;lt;/span&amp;gt;
                    &amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;!-- 일반 사용자 대시보드 --&amp;gt;
            &amp;lt;div sec:authorize=&quot;hasRole('USER')&quot; class=&quot;col-md-6&quot;&amp;gt;
                &amp;lt;div class=&quot;card&quot;&amp;gt;
                    &amp;lt;div class=&quot;card-header&quot;&amp;gt;
                        &amp;lt;h5&amp;gt;내 활동&amp;lt;/h5&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                        &amp;lt;ul class=&quot;list-unstyled&quot;&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/orders}&quot;&amp;gt;주문 내역 (&amp;lt;span th:text=&quot;${userStats.orderCount}&quot;&amp;gt;0&amp;lt;/span&amp;gt;)&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/reviews}&quot;&amp;gt;내 리뷰 (&amp;lt;span th:text=&quot;${userStats.reviewCount}&quot;&amp;gt;0&amp;lt;/span&amp;gt;)&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/wishlist}&quot;&amp;gt;찜 목록 (&amp;lt;span th:text=&quot;${userStats.wishlistCount}&quot;&amp;gt;0&amp;lt;/span&amp;gt;)&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/profile}&quot;&amp;gt;프로필 관리&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                        &amp;lt;/ul&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 관리자 전용 대시보드 --&amp;gt;
            &amp;lt;div sec:authorize=&quot;hasRole('ADMIN')&quot; class=&quot;col-md-6&quot;&amp;gt;
                &amp;lt;div class=&quot;card border-danger&quot;&amp;gt;
                    &amp;lt;div class=&quot;card-header bg-danger text-white&quot;&amp;gt;
                        &amp;lt;h5&amp;gt;관리자 도구&amp;lt;/h5&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                        &amp;lt;div class=&quot;row text-center&quot;&amp;gt;
                            &amp;lt;div class=&quot;col-4&quot;&amp;gt;
                                &amp;lt;div class=&quot;stat-box&quot;&amp;gt;
                                    &amp;lt;h3 th:text=&quot;${adminStats.totalUsers}&quot;&amp;gt;1,234&amp;lt;/h3&amp;gt;
                                    &amp;lt;p&amp;gt;총 사용자&amp;lt;/p&amp;gt;
                                    &amp;lt;a th:href=&quot;@{/admin/users}&quot; class=&quot;btn btn-sm btn-outline-primary&quot;&amp;gt;관리&amp;lt;/a&amp;gt;
                                &amp;lt;/div&amp;gt;
                            &amp;lt;/div&amp;gt;
                            &amp;lt;div class=&quot;col-4&quot;&amp;gt;
                                &amp;lt;div class=&quot;stat-box&quot;&amp;gt;
                                    &amp;lt;h3 th:text=&quot;${adminStats.totalProducts}&quot;&amp;gt;567&amp;lt;/h3&amp;gt;
                                    &amp;lt;p&amp;gt;총 상품&amp;lt;/p&amp;gt;
                                    &amp;lt;a th:href=&quot;@{/admin/products}&quot; class=&quot;btn btn-sm btn-outline-success&quot;&amp;gt;관리&amp;lt;/a&amp;gt;
                                &amp;lt;/div&amp;gt;
                            &amp;lt;/div&amp;gt;
                            &amp;lt;div class=&quot;col-4&quot;&amp;gt;
                                &amp;lt;div class=&quot;stat-box&quot;&amp;gt;
                                    &amp;lt;h3 th:text=&quot;${adminStats.totalOrders}&quot;&amp;gt;890&amp;lt;/h3&amp;gt;
                                    &amp;lt;p&amp;gt;총 주문&amp;lt;/p&amp;gt;
                                    &amp;lt;a th:href=&quot;@{/admin/orders}&quot; class=&quot;btn btn-sm btn-outline-info&quot;&amp;gt;관리&amp;lt;/a&amp;gt;
                                &amp;lt;/div&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 운영자 전용 도구 --&amp;gt;
            &amp;lt;div sec:authorize=&quot;hasRole('MODERATOR')&quot; class=&quot;col-md-6&quot;&amp;gt;
                &amp;lt;div class=&quot;card border-warning&quot;&amp;gt;
                    &amp;lt;div class=&quot;card-header bg-warning&quot;&amp;gt;
                        &amp;lt;h5&amp;gt;운영자 도구&amp;lt;/h5&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                        &amp;lt;ul class=&quot;list-unstyled&quot;&amp;gt;
                            &amp;lt;li&amp;gt;
                                &amp;lt;a th:href=&quot;@{/moderate/posts}&quot;&amp;gt;
                                    게시글 검토 
                                    &amp;lt;span th:if=&quot;${moderationStats.pendingPosts &amp;gt; 0}&quot; 
                                          class=&quot;badge bg-warning&quot; 
                                          th:text=&quot;${moderationStats.pendingPosts}&quot;&amp;gt;5&amp;lt;/span&amp;gt;
                                &amp;lt;/a&amp;gt;
                            &amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;
                                &amp;lt;a th:href=&quot;@{/moderate/reviews}&quot;&amp;gt;
                                    리뷰 검토
                                    &amp;lt;span th:if=&quot;${moderationStats.pendingReviews &amp;gt; 0}&quot; 
                                          class=&quot;badge bg-warning&quot; 
                                          th:text=&quot;${moderationStats.pendingReviews}&quot;&amp;gt;3&amp;lt;/span&amp;gt;
                                &amp;lt;/a&amp;gt;
                            &amp;lt;/li&amp;gt;
                            &amp;lt;li&amp;gt;
                                &amp;lt;a th:href=&quot;@{/moderate/reports}&quot;&amp;gt;
                                    신고 처리
                                    &amp;lt;span th:if=&quot;${moderationStats.pendingReports &amp;gt; 0}&quot; 
                                          class=&quot;badge bg-danger&quot; 
                                          th:text=&quot;${moderationStats.pendingReports}&quot;&amp;gt;2&amp;lt;/span&amp;gt;
                                &amp;lt;/a&amp;gt;
                            &amp;lt;/li&amp;gt;
                        &amp;lt;/ul&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 최근 활동 (모든 사용자) --&amp;gt;
            &amp;lt;div class=&quot;col-12 mt-4&quot;&amp;gt;
                &amp;lt;div class=&quot;card&quot;&amp;gt;
                    &amp;lt;div class=&quot;card-header&quot;&amp;gt;
                        &amp;lt;h5&amp;gt;최근 활동&amp;lt;/h5&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                        &amp;lt;div th:if=&quot;${#lists.isEmpty(recentActivities)}&quot; class=&quot;text-center text-muted&quot;&amp;gt;
                            &amp;lt;p&amp;gt;최근 활동이 없습니다.&amp;lt;/p&amp;gt;
                        &amp;lt;/div&amp;gt;

                        &amp;lt;div th:unless=&quot;${#lists.isEmpty(recentActivities)}&quot;&amp;gt;
                            &amp;lt;div th:each=&quot;activity : ${recentActivities}&quot; class=&quot;activity-item mb-3&quot;&amp;gt;
                                &amp;lt;div class=&quot;d-flex&quot;&amp;gt;
                                    &amp;lt;div class=&quot;activity-icon me-3&quot;&amp;gt;
                                        &amp;lt;i th:class=&quot;${activity.iconClass}&quot; 
                                           th:classappend=&quot;${activity.type}&quot;&amp;gt;&amp;lt;/i&amp;gt;
                                    &amp;lt;/div&amp;gt;
                                    &amp;lt;div class=&quot;activity-content flex-grow-1&quot;&amp;gt;
                                        &amp;lt;p class=&quot;mb-1&quot; th:text=&quot;${activity.description}&quot;&amp;gt;활동 설명&amp;lt;/p&amp;gt;
                                        &amp;lt;small class=&quot;text-muted&quot; 
                                               th:text=&quot;${#temporals.format(activity.timestamp, 'yyyy-MM-dd HH:mm')}&quot;&amp;gt;
                                               2024-01-15 14:30
                                        &amp;lt;/small&amp;gt;
                                    &amp;lt;/div&amp;gt;
                                &amp;lt;/div&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 권한별 추가 정보 --&amp;gt;
        &amp;lt;div class=&quot;row mt-4&quot;&amp;gt;
            &amp;lt;div class=&quot;col-12&quot;&amp;gt;
                &amp;lt;div class=&quot;alert alert-info&quot; sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;
                    &amp;lt;h6&amp;gt;관리자 알림&amp;lt;/h6&amp;gt;
                    &amp;lt;p&amp;gt;시스템 상태가 정상입니다. 
                       대기 중인 작업: &amp;lt;strong th:text=&quot;${adminStats.pendingTasks}&quot;&amp;gt;0&amp;lt;/strong&amp;gt;개&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&quot;alert alert-warning&quot; sec:authorize=&quot;hasRole('MODERATOR')&quot;&amp;gt;
                    &amp;lt;h6&amp;gt;운영자 알림&amp;lt;/h6&amp;gt;
                    &amp;lt;p&amp;gt;검토가 필요한 콘텐츠가 있습니다. 
                       총 &amp;lt;strong th:text=&quot;${moderationStats.totalPending}&quot;&amp;gt;0&amp;lt;/strong&amp;gt;개의 항목이 대기 중입니다.&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&quot;alert alert-success&quot; sec:authorize=&quot;hasRole('USER') and !hasRole('MODERATOR') and !hasRole('ADMIN')&quot;&amp;gt;
                    &amp;lt;h6&amp;gt;환영합니다!&amp;lt;/h6&amp;gt;
                    &amp;lt;p&amp;gt;MyApp의 모든 기능을 자유롭게 이용하실 수 있습니다.&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:replace=&quot;~{fragments/common :: footer}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;보안 관련 유틸리티 메소드&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 현재 사용자 정보 접근 --&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;!-- 인증 상태 확인 --&amp;gt;
    &amp;lt;span sec:authorize=&quot;isAuthenticated()&quot;&amp;gt;로그인됨&amp;lt;/span&amp;gt;
    &amp;lt;span sec:authorize=&quot;!isAuthenticated()&quot;&amp;gt;로그인 안됨&amp;lt;/span&amp;gt;

    &amp;lt;!-- 사용자명 표시 --&amp;gt;
    &amp;lt;span sec:authentication=&quot;name&quot;&amp;gt;username&amp;lt;/span&amp;gt;

    &amp;lt;!-- 사용자 권한 표시 --&amp;gt;
    &amp;lt;span sec:authentication=&quot;principal.authorities&quot;&amp;gt;[ROLE_USER, ROLE_ADMIN]&amp;lt;/span&amp;gt;

    &amp;lt;!-- 특정 권한 확인 --&amp;gt;
    &amp;lt;div sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;관리자만 볼 수 있음&amp;lt;/div&amp;gt;
    &amp;lt;div sec:authorize=&quot;hasAuthority('WRITE_PRIVILEGE')&quot;&amp;gt;쓰기 권한이 있는 사용자만&amp;lt;/div&amp;gt;

    &amp;lt;!-- 복합 권한 확인 --&amp;gt;
    &amp;lt;div sec:authorize=&quot;hasRole('ADMIN') or hasRole('MODERATOR')&quot;&amp;gt;관리자 또는 운영자만&amp;lt;/div&amp;gt;
    &amp;lt;div sec:authorize=&quot;hasRole('USER') and !hasRole('BANNED')&quot;&amp;gt;정상 사용자만&amp;lt;/div&amp;gt;

    &amp;lt;!-- 자신의 리소스만 접근 허용 --&amp;gt;
    &amp;lt;div sec:authorize=&quot;@securityService.canAccessUser(authentication.name, #user.username)&quot;&amp;gt;
        개인 정보 수정 가능
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 Spring Boot 자동 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 Thymeleaf 통합을 위한 강력한 자동 설정을 제공합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;자동 설정 활성화&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
spring:
  thymeleaf:
    # 템플릿 캐싱 (운영: true, 개발: false)
    cache: false

    # 템플릿 위치 설정
    prefix: classpath:/templates/
    suffix: .html

    # 템플릿 모드
    mode: HTML

    # 문자 인코딩
    encoding: UTF-8

    # 템플릿 존재 확인
    check-template: true
    check-template-location: true

    # 렌더링 전 템플릿 존재 확인
    enabled: true

    # Spring EL 컴파일러 사용
    enable-spring-el-compiler: true

    # 뷰 이름 패턴 (선택적)
    view-names: &quot;*.html,*.xhtml&quot;

    # 제외할 뷰 이름 패턴
    excluded-view-names: &quot;*.txt,*.xml&quot;

# 프로파일별 설정
---
spring:
  config:
    activate:
      on-profile: development
  thymeleaf:
    cache: false
  devtools:
    restart:
      enabled: true
    livereload:
      enabled: true

---
spring:
  config:
    activate:
      on-profile: production
  thymeleaf:
    cache: true&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;커스텀 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// src/main/java/com/example/config/ThymeleafConfig.java
@Configuration
public class ThymeleafConfig {

    @Bean
    @Primary
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setEnableSpringELCompiler(true);

        // 커스텀 Dialect 추가
        engine.addDialect(new CustomDialect());

        // 추가 유틸리티 객체 등록
        engine.addTemplateResolver(templateResolver());

        return engine;
    }

    @Bean
    public ITemplateResolver templateResolver() {
        SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
        resolver.setApplicationContext(applicationContext);
        resolver.setPrefix(&quot;classpath:/templates/&quot;);
        resolver.setSuffix(&quot;.html&quot;);
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding(&quot;UTF-8&quot;);
        resolver.setCacheable(false);
        resolver.setOrder(1);
        return resolver;
    }

    @Autowired
    private ApplicationContext applicationContext;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;메시지 소스 통합&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;// src/main/java/com/example/config/MessageConfig.java
@Configuration
public class MessageConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = 
            new ReloadableResourceBundleMessageSource();

        messageSource.setBasenames(
            &quot;classpath:messages/messages&quot;,
            &quot;classpath:messages/validation&quot;,
            &quot;classpath:messages/errors&quot;
        );

        messageSource.setDefaultEncoding(&quot;UTF-8&quot;);
        messageSource.setCacheSeconds(300); // 5분 캐시
        messageSource.setFallbackToSystemLocale(false);

        return messageSource;
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(Locale.KOREAN);
        return resolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName(&quot;lang&quot;);
        return interceptor;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;메시지 파일&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# src/main/resources/messages/messages.properties (기본)
app.name=MyApp
app.description=최고의 온라인 쇼핑몰
app.welcome=MyApp에 오신 것을 환영합니다!

menu.home=홈
menu.products=상품
menu.about=회사소개
menu.contact=문의
menu.login=로그인
menu.logout=로그아웃
menu.signup=회원가입

page.home.title=홈
page.products.title=상품 목록
page.contact.title=문의하기

button.submit=제출
button.cancel=취소
button.save=저장
button.delete=삭제
button.edit=수정&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# src/main/resources/messages/messages_en.properties (영어)
app.name=MyApp
app.description=The Best Online Shopping Mall
app.welcome=Welcome to MyApp!

menu.home=Home
menu.products=Products
menu.about=About
menu.contact=Contact
menu.login=Login
menu.logout=Logout
menu.signup=Sign Up

page.home.title=Home
page.products.title=Product List
page.contact.title=Contact Us

button.submit=Submit
button.cancel=Cancel
button.save=Save
button.delete=Delete
button.edit=Edit&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;국제화 활용 예제&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/fragments/language-switcher.html --&amp;gt;
&amp;lt;div th:fragment=&quot;languageSwitcher&quot; class=&quot;language-switcher&quot;&amp;gt;
    &amp;lt;div class=&quot;dropdown&quot;&amp;gt;
        &amp;lt;button class=&quot;btn btn-link dropdown-toggle&quot; type=&quot;button&quot; data-bs-toggle=&quot;dropdown&quot;&amp;gt;
            &amp;lt;i class=&quot;fas fa-globe&quot;&amp;gt;&amp;lt;/i&amp;gt;
            &amp;lt;span th:text=&quot;#{language.current}&quot;&amp;gt;한국어&amp;lt;/span&amp;gt;
        &amp;lt;/button&amp;gt;
        &amp;lt;ul class=&quot;dropdown-menu&quot;&amp;gt;
            &amp;lt;li&amp;gt;
                &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{''(lang='ko')}&quot;&amp;gt;
                    &amp;lt;img src=&quot;/images/flags/kr.png&quot; alt=&quot;한국어&quot; class=&quot;flag-icon&quot;&amp;gt; 한국어
                &amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;
            &amp;lt;li&amp;gt;
                &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{''(lang='en')}&quot;&amp;gt;
                    &amp;lt;img src=&quot;/images/flags/us.png&quot; alt=&quot;English&quot; class=&quot;flag-icon&quot;&amp;gt; English
                &amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;
            &amp;lt;li&amp;gt;
                &amp;lt;a class=&quot;dropdown-item&quot; th:href=&quot;@{''(lang='ja')}&quot;&amp;gt;
                    &amp;lt;img src=&quot;/images/flags/jp.png&quot; alt=&quot;日本語&quot; class=&quot;flag-icon&quot;&amp;gt; 日本語
                &amp;lt;/a&amp;gt;
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;u&gt;7.4 커스텀 설정과 프로퍼티&lt;/u&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;애플리케이션 프로퍼티 정의&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// src/main/java/com/example/config/AppProperties.java
@ConfigurationProperties(prefix = &quot;app&quot;)
@Component
@Data
public class AppProperties {

    private String name = &quot;MyApp&quot;;
    private String version = &quot;1.0.0&quot;;
    private String description = &quot;온라인 쇼핑몰&quot;;

    private Contact contact = new Contact();
    private Features features = new Features();
    private Upload upload = new Upload();

    @Data
    public static class Contact {
        private String email = &quot;info@myapp.com&quot;;
        private String phone = &quot;02-1234-5678&quot;;
        private String address = &quot;서울시 강남구 테헤란로 123&quot;;
    }

    @Data
    public static class Features {
        private boolean enableReviews = true;
        private boolean enableWishlist = true;
        private boolean enableNotifications = true;
        private int maxReviewLength = 1000;
        private int productsPerPage = 12;
    }

    @Data
    public static class Upload {
        private String path = &quot;/uploads/&quot;;
        private long maxFileSize = 5242880; // 5MB
        private String[] allowedExtensions = {&quot;jpg&quot;, &quot;jpeg&quot;, &quot;png&quot;, &quot;gif&quot;};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
app:
  name: MyApp
  version: 1.2.0
  description: 최고의 온라인 쇼핑몰
  contact:
    email: contact@myapp.com
    phone: 02-9876-5432
    address: 서울시 서초구 강남대로 456
  features:
    enable-reviews: true
    enable-wishlist: true
    enable-notifications: true
    max-review-length: 500
    products-per-page: 16
  upload:
    path: /app/uploads/
    max-file-size: 10485760  # 10MB
    allowed-extensions:
      - jpg
      - jpeg
      - png
      - gif
      - webp&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;템플릿에서 프로퍼티 사용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/fragments/app-info.html --&amp;gt;
&amp;lt;div th:fragment=&quot;appInfo&quot; class=&quot;app-info&quot;&amp;gt;
    &amp;lt;h1 th:text=&quot;${@appProperties.name}&quot;&amp;gt;MyApp&amp;lt;/h1&amp;gt;
    &amp;lt;p th:text=&quot;${@appProperties.description}&quot;&amp;gt;앱 설명&amp;lt;/p&amp;gt;
    &amp;lt;small&amp;gt;Version &amp;lt;span th:text=&quot;${@appProperties.version}&quot;&amp;gt;1.0.0&amp;lt;/span&amp;gt;&amp;lt;/small&amp;gt;

    &amp;lt;div class=&quot;contact-info&quot;&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;i class=&quot;fas fa-envelope&quot;&amp;gt;&amp;lt;/i&amp;gt;
            &amp;lt;a th:href=&quot;'mailto:' + ${@appProperties.contact.email}&quot; 
               th:text=&quot;${@appProperties.contact.email}&quot;&amp;gt;contact@myapp.com&amp;lt;/a&amp;gt;
        &amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;i class=&quot;fas fa-phone&quot;&amp;gt;&amp;lt;/i&amp;gt;
            &amp;lt;span th:text=&quot;${@appProperties.contact.phone}&quot;&amp;gt;02-1234-5678&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;i class=&quot;fas fa-map-marker-alt&quot;&amp;gt;&amp;lt;/i&amp;gt;
            &amp;lt;span th:text=&quot;${@appProperties.contact.address}&quot;&amp;gt;서울시 강남구&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 기능 플래그 사용 --&amp;gt;
&amp;lt;div th:fragment=&quot;conditionalFeatures&quot;&amp;gt;
    &amp;lt;!-- 리뷰 기능이 활성화된 경우만 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${@appProperties.features.enableReviews}&quot; class=&quot;reviews-section&quot;&amp;gt;
        &amp;lt;h3&amp;gt;상품 리뷰&amp;lt;/h3&amp;gt;
        &amp;lt;p&amp;gt;최대 &amp;lt;span th:text=&quot;${@appProperties.features.maxReviewLength}&quot;&amp;gt;1000&amp;lt;/span&amp;gt;자까지 입력 가능합니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 찜하기 기능이 활성화된 경우만 표시 --&amp;gt;
    &amp;lt;button th:if=&quot;${@appProperties.features.enableWishlist}&quot; 
            class=&quot;btn btn-outline-heart&quot;&amp;gt;
        &amp;lt;i class=&quot;fas fa-heart&quot;&amp;gt;&amp;lt;/i&amp;gt; 찜하기
    &amp;lt;/button&amp;gt;

    &amp;lt;!-- 알림 기능이 활성화된 경우만 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${@appProperties.features.enableNotifications}&quot; class=&quot;notification-bell&quot;&amp;gt;
        &amp;lt;i class=&quot;fas fa-bell&quot;&amp;gt;&amp;lt;/i&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;환경별 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application-development.yml
spring:
  thymeleaf:
    cache: false
  h2:
    console:
      enabled: true
logging:
  level:
    com.example: DEBUG
    org.thymeleaf: DEBUG

app:
  features:
    enable-notifications: false  # 개발 환경에서는 알림 비활성화

---
# application-production.yml
spring:
  thymeleaf:
    cache: true
logging:
  level:
    com.example: INFO
    org.thymeleaf: WARN

app:
  upload:
    path: /var/app/uploads/
    max-file-size: 20971520  # 20MB (운영환경에서 더 큰 용량)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;커스텀 유틸리티 빈&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;// src/main/java/com/example/service/ThymeleafUtilityService.java
@Service(&quot;utilService&quot;)
public class ThymeleafUtilityService {

    @Autowired
    private AppProperties appProperties;

    public String formatFileSize(long bytes) {
        if (bytes &amp;lt; 1024) return bytes + &quot; B&quot;;
        int exp = (int) (Math.log(bytes) / Math.log(1024));
        String pre = &quot;KMGTPE&quot;.charAt(exp - 1) + &quot;i&quot;;
        return String.format(&quot;%.1f %sB&quot;, bytes / Math.pow(1024, exp), pre);
    }

    public boolean isFeatureEnabled(String featureName) {
        switch (featureName.toLowerCase()) {
            case &quot;reviews&quot;: return appProperties.getFeatures().isEnableReviews();
            case &quot;wishlist&quot;: return appProperties.getFeatures().isEnableWishlist();
            case &quot;notifications&quot;: return appProperties.getFeatures().isEnableNotifications();
            default: return false;
        }
    }

    public String truncateText(String text, int maxLength) {
        if (text == null || text.length() &amp;lt;= maxLength) {
            return text;
        }
        return text.substring(0, maxLength - 3) + &quot;...&quot;;
    }

    public String getUploadUrl(String filename) {
        return appProperties.getUpload().getPath() + filename;
    }

    public boolean isValidFileExtension(String filename) {
        String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
        return Arrays.asList(appProperties.getUpload().getAllowedExtensions()).contains(extension);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;템플릿에서 커스텀 유틸리티 사용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/products/detail.html --&amp;gt;
&amp;lt;div class=&quot;product-detail&quot;&amp;gt;
    &amp;lt;!-- 기능 플래그 기반 조건부 렌더링 --&amp;gt;
    &amp;lt;div th:if=&quot;${@utilService.isFeatureEnabled('reviews')}&quot; class=&quot;reviews-section&quot;&amp;gt;
        &amp;lt;h3&amp;gt;상품 리뷰&amp;lt;/h3&amp;gt;
        &amp;lt;div th:each=&quot;review : ${reviews}&quot; class=&quot;review&quot;&amp;gt;
            &amp;lt;h5 th:text=&quot;${review.author}&quot;&amp;gt;작성자&amp;lt;/h5&amp;gt;
            &amp;lt;!-- 텍스트 길이 제한 --&amp;gt;
            &amp;lt;p th:text=&quot;${@utilService.truncateText(review.content, 200)}&quot;&amp;gt;리뷰 내용&amp;lt;/p&amp;gt;
            &amp;lt;small th:text=&quot;${#temporals.format(review.createdAt, 'yyyy-MM-dd')}&quot;&amp;gt;2024-01-15&amp;lt;/small&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 찜하기 버튼 (기능 활성화 시에만) --&amp;gt;
    &amp;lt;button th:if=&quot;${@utilService.isFeatureEnabled('wishlist')}&quot; 
            class=&quot;btn btn-outline-danger&quot;&amp;gt;
        &amp;lt;i class=&quot;fas fa-heart&quot;&amp;gt;&amp;lt;/i&amp;gt; 찜하기
    &amp;lt;/button&amp;gt;

    &amp;lt;!-- 파일 업로드 정보 --&amp;gt;
    &amp;lt;div class=&quot;upload-info&quot;&amp;gt;
        &amp;lt;p&amp;gt;최대 업로드 크기: 
           &amp;lt;span th:text=&quot;${@utilService.formatFileSize(@appProperties.upload.maxFileSize)}&quot;&amp;gt;5.0 MiB&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;허용 확장자: 
           &amp;lt;span th:text=&quot;${#strings.listJoin(@appProperties.upload.allowedExtensions, ', ')}&quot;&amp;gt;jpg, png&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Spring Boot Actuator 통합&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- pom.xml --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,thymeleaf
  endpoint:
    health:
      show-details: when_authorized&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/admin/system-info.html --&amp;gt;
&amp;lt;div class=&quot;system-info&quot; sec:authorize=&quot;hasRole('ADMIN')&quot;&amp;gt;
    &amp;lt;h2&amp;gt;시스템 정보&amp;lt;/h2&amp;gt;

    &amp;lt;div class=&quot;row&quot;&amp;gt;
        &amp;lt;div class=&quot;col-md-4&quot;&amp;gt;
            &amp;lt;div class=&quot;card&quot;&amp;gt;
                &amp;lt;div class=&quot;card-header&quot;&amp;gt;애플리케이션&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                    &amp;lt;p&amp;gt;이름: &amp;lt;span th:text=&quot;${@appProperties.name}&quot;&amp;gt;MyApp&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;p&amp;gt;버전: &amp;lt;span th:text=&quot;${@appProperties.version}&quot;&amp;gt;1.0.0&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;p&amp;gt;프로파일: &amp;lt;span th:text=&quot;${@environment.getActiveProfiles()[0]}&quot;&amp;gt;development&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;col-md-4&quot;&amp;gt;
            &amp;lt;div class=&quot;card&quot;&amp;gt;
                &amp;lt;div class=&quot;card-header&quot;&amp;gt;Thymeleaf 설정&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                    &amp;lt;p&amp;gt;캐시: &amp;lt;span th:text=&quot;${@environment.getProperty('spring.thymeleaf.cache')}&quot;&amp;gt;false&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;p&amp;gt;모드: &amp;lt;span th:text=&quot;${@environment.getProperty('spring.thymeleaf.mode')}&quot;&amp;gt;HTML&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;p&amp;gt;인코딩: &amp;lt;span th:text=&quot;${@environment.getProperty('spring.thymeleaf.encoding')}&quot;&amp;gt;UTF-8&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;col-md-4&quot;&amp;gt;
            &amp;lt;div class=&quot;card&quot;&amp;gt;
                &amp;lt;div class=&quot;card-header&quot;&amp;gt;기능 상태&amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                    &amp;lt;p&amp;gt;리뷰: 
                       &amp;lt;span th:class=&quot;${@utilService.isFeatureEnabled('reviews')} ? 'text-success' : 'text-danger'&quot;
                             th:text=&quot;${@utilService.isFeatureEnabled('reviews')} ? '활성' : '비활성'&quot;&amp;gt;활성&amp;lt;/span&amp;gt;
                    &amp;lt;/p&amp;gt;
                    &amp;lt;p&amp;gt;찜하기: 
                       &amp;lt;span th:class=&quot;${@utilService.isFeatureEnabled('wishlist')} ? 'text-success' : 'text-danger'&quot;
                             th:text=&quot;${@utilService.isFeatureEnabled('wishlist')} ? '활성' : '비활성'&quot;&amp;gt;활성&amp;lt;/span&amp;gt;
                    &amp;lt;/p&amp;gt;
                    &amp;lt;p&amp;gt;알림: 
                       &amp;lt;span th:class=&quot;${@utilService.isFeatureEnabled('notifications')} ? 'text-success' : 'text-danger'&quot;
                             th:text=&quot;${@utilService.isFeatureEnabled('notifications')} ? '활성' : '비활성'&quot;&amp;gt;활성&amp;lt;/span&amp;gt;
                    &amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;전역 모델 속성&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// src/main/java/com/example/controller/GlobalControllerAdvice.java
@ControllerAdvice
public class GlobalControllerAdvice {

    @Autowired
    private AppProperties appProperties;

    @Autowired
    private CategoryService categoryService;

    @ModelAttribute(&quot;appInfo&quot;)
    public AppProperties appInfo() {
        return appProperties;
    }

    @ModelAttribute(&quot;globalCategories&quot;)
    public List&amp;lt;Category&amp;gt; globalCategories() {
        return categoryService.getAllCategories();
    }

    @ModelAttribute(&quot;currentYear&quot;)
    public int currentYear() {
        return LocalDate.now().getYear();
    }

    @ModelAttribute(&quot;isProduction&quot;)
    public boolean isProduction(@Value(&quot;${spring.profiles.active:default}&quot;) String profile) {
        return &quot;production&quot;.equals(profile);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모든 템플릿에서 이 전역 속성들을 사용할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 어떤 템플릿에서든 사용 가능 --&amp;gt;
&amp;lt;footer&amp;gt;
    &amp;lt;p&amp;gt;&amp;copy; &amp;lt;span th:text=&quot;${currentYear}&quot;&amp;gt;2024&amp;lt;/span&amp;gt; &amp;lt;span th:text=&quot;${appInfo.name}&quot;&amp;gt;MyApp&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;${isProduction}&quot;&amp;gt;개발 환경에서 실행 중&amp;lt;/p&amp;gt;
&amp;lt;/footer&amp;gt;

&amp;lt;nav&amp;gt;
    &amp;lt;ul&amp;gt;
        &amp;lt;li th:each=&quot;category : ${globalCategories}&quot;&amp;gt;
            &amp;lt;a th:href=&quot;@{/products(category=${category.id})}&quot; th:text=&quot;${category.name}&quot;&amp;gt;카테고리&amp;lt;/a&amp;gt;
        &amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
&amp;lt;/nav&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;정리&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7장에서는 Spring Framework와 Thymeleaf의 완전한 통합에 대해 알아보았습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;핵심 내용 요약&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring MVC 연동&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨트롤러에서 Model을 통한 데이터 전달&lt;/li&gt;
&lt;li&gt;뷰 이름 해석과 템플릿 렌더링&lt;/li&gt;
&lt;li&gt;폼 처리와 검증 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Security 통합&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증과 권한 기반 UI 구성&lt;/li&gt;
&lt;li&gt;보안 네임스페이스 사용법&lt;/li&gt;
&lt;li&gt;사용자별 맞춤형 인터페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Boot 자동 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;간편한 의존성 관리&lt;/li&gt;
&lt;li&gt;프로파일별 설정 관리&lt;/li&gt;
&lt;li&gt;국제화와 메시지 소스 통합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스텀 설정과 확장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션 프로퍼티 활용&lt;/li&gt;
&lt;li&gt;커스텀 유틸리티 서비스&lt;/li&gt;
&lt;li&gt;전역 모델 속성 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 통합을 통해 Spring의 강력한 기능들을 Thymeleaf 템플릿에서 완전히 활용할 수 있으며, 보안과 설정 관리가 용이한 웹 애플리케이션을 구축할 수 있습니다.&lt;/p&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/146</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-7%EC%9E%A5-Spring%EA%B3%BC%EC%9D%98-%ED%86%B5%ED%95%A9#entry146comment</comments>
      <pubDate>Tue, 26 Aug 2025 11:08:54 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #6장. 템플릿 레이아웃과 프래그먼트</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-6%EC%9E%A5-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83%EA%B3%BC-%ED%94%84%EB%9E%98%EA%B7%B8%EB%A8%BC%ED%8A%B8</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nXgLn/btsP4mT4d3L/P2lBtQHRdcwrJwL99exJmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nXgLn/btsP4mT4d3L/P2lBtQHRdcwrJwL99exJmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nXgLn/btsP4mT4d3L/P2lBtQHRdcwrJwL99exJmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnXgLn%2FbtsP4mT4d3L%2FP2lBtQHRdcwrJwL99exJmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6장.&amp;nbsp;템플릿&amp;nbsp;레이아웃과&amp;nbsp;프래그먼트&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 프래그먼트 정의와 사용 (&lt;code&gt;th:fragment&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프래그먼트는 재사용 가능한 HTML 조각으로, Thymeleaf에서 코드 중복을 줄이고 유지보수성을 높이는 핵심 기능입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;프래그먼트란?&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프래그먼트는 &lt;code&gt;th:fragment&lt;/code&gt; 속성으로 정의되는 HTML의 재사용 가능한 부분입니다. 헤더, 푸터, 네비게이션 등 여러 페이지에서 공통으로 사용되는 부분을 프래그먼트로 만들어 관리할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 프래그먼트 정의&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/fragments/common.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;

&amp;lt;!-- 헤더 프래그먼트 --&amp;gt;
&amp;lt;header th:fragment=&quot;header&quot; class=&quot;site-header&quot;&amp;gt;
    &amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;nav class=&quot;navbar&quot;&amp;gt;
            &amp;lt;div class=&quot;navbar-brand&quot;&amp;gt;
                &amp;lt;a th:href=&quot;@{/}&quot;&amp;gt;
                    &amp;lt;img src=&quot;/images/logo.png&quot; alt=&quot;Logo&quot;&amp;gt;
                    &amp;lt;span th:text=&quot;#{site.name}&quot;&amp;gt;MyApp&amp;lt;/span&amp;gt;
                &amp;lt;/a&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;ul class=&quot;navbar-nav&quot;&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/}&quot;&amp;gt;홈&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/products}&quot;&amp;gt;상품&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/about}&quot;&amp;gt;회사소개&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/contact}&quot;&amp;gt;문의&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;

            &amp;lt;div class=&quot;navbar-user&quot;&amp;gt;
                &amp;lt;div th:if=&quot;${currentUser}&quot;&amp;gt;
                    &amp;lt;span th:text=&quot;|안녕하세요, ${currentUser.name}님|&quot;&amp;gt;사용자&amp;lt;/span&amp;gt;
                    &amp;lt;a th:href=&quot;@{/logout}&quot;&amp;gt;로그아웃&amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div th:unless=&quot;${currentUser}&quot;&amp;gt;
                    &amp;lt;a th:href=&quot;@{/login}&quot;&amp;gt;로그인&amp;lt;/a&amp;gt;
                    &amp;lt;a th:href=&quot;@{/signup}&quot;&amp;gt;회원가입&amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/nav&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/header&amp;gt;

&amp;lt;!-- 푸터 프래그먼트 --&amp;gt;
&amp;lt;footer th:fragment=&quot;footer&quot; class=&quot;site-footer&quot;&amp;gt;
    &amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;div class=&quot;footer-content&quot;&amp;gt;
            &amp;lt;div class=&quot;footer-section&quot;&amp;gt;
                &amp;lt;h3&amp;gt;회사 정보&amp;lt;/h3&amp;gt;
                &amp;lt;p&amp;gt;우리 회사는 최고의 서비스를 제공합니다.&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;Email: info@company.com&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;Tel: 02-1234-5678&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div class=&quot;footer-section&quot;&amp;gt;
                &amp;lt;h3&amp;gt;빠른 링크&amp;lt;/h3&amp;gt;
                &amp;lt;ul&amp;gt;
                    &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/privacy}&quot;&amp;gt;개인정보처리방침&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                    &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/terms}&quot;&amp;gt;이용약관&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                    &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/help}&quot;&amp;gt;고객지원&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div class=&quot;footer-section&quot;&amp;gt;
                &amp;lt;h3&amp;gt;소셜 미디어&amp;lt;/h3&amp;gt;
                &amp;lt;div class=&quot;social-links&quot;&amp;gt;
                    &amp;lt;a href=&quot;#&quot; class=&quot;social-facebook&quot;&amp;gt;Facebook&amp;lt;/a&amp;gt;
                    &amp;lt;a href=&quot;#&quot; class=&quot;social-instagram&quot;&amp;gt;Instagram&amp;lt;/a&amp;gt;
                    &amp;lt;a href=&quot;#&quot; class=&quot;social-twitter&quot;&amp;gt;Twitter&amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;footer-bottom&quot;&amp;gt;
            &amp;lt;p th:text=&quot;|&amp;copy; ${#dates.year(#dates.createNow())} MyApp. All rights reserved.|&quot;&amp;gt;
                &amp;copy; 2024 MyApp. All rights reserved.
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/footer&amp;gt;

&amp;lt;!-- 사이드바 프래그먼트 --&amp;gt;
&amp;lt;aside th:fragment=&quot;sidebar&quot; class=&quot;sidebar&quot;&amp;gt;
    &amp;lt;!-- 최근 게시글 위젯 --&amp;gt;
    &amp;lt;div class=&quot;widget&quot;&amp;gt;
        &amp;lt;h3&amp;gt;최근 게시글&amp;lt;/h3&amp;gt;
        &amp;lt;ul class=&quot;recent-posts&quot;&amp;gt;
            &amp;lt;li th:each=&quot;post : ${recentPosts}&quot;&amp;gt;
                &amp;lt;a th:href=&quot;@{/posts/{id}(id=${post.id})}&quot; th:text=&quot;${post.title}&quot;&amp;gt;게시글 제목&amp;lt;/a&amp;gt;
                &amp;lt;small th:text=&quot;${#temporals.format(post.createdAt, 'MM-dd')}&quot;&amp;gt;01-15&amp;lt;/small&amp;gt;
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 카테고리 위젯 --&amp;gt;
    &amp;lt;div class=&quot;widget&quot;&amp;gt;
        &amp;lt;h3&amp;gt;카테고리&amp;lt;/h3&amp;gt;
        &amp;lt;ul class=&quot;categories&quot;&amp;gt;
            &amp;lt;li th:each=&quot;category : ${categories}&quot;&amp;gt;
                &amp;lt;a th:href=&quot;@{/category/{id}(id=${category.id})}&quot; th:text=&quot;${category.name}&quot;&amp;gt;카테고리&amp;lt;/a&amp;gt;
                &amp;lt;span th:text=&quot;|( ${category.postCount} )|&quot;&amp;gt; (5)&amp;lt;/span&amp;gt;
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/aside&amp;gt;

&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;프래그먼트 사용하기&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프래그먼트를 사용할 때는 &lt;code&gt;~{템플릿경로 :: 프래그먼트명}&lt;/code&gt; 형식으로 참조합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/home.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;홈 - MyApp&amp;lt;/title&amp;gt;
    &amp;lt;link href=&quot;/css/main.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!-- 헤더 프래그먼트 삽입 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: header}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;div class=&quot;row&quot;&amp;gt;
            &amp;lt;!-- 메인 콘텐츠 --&amp;gt;
            &amp;lt;main class=&quot;col-md-9&quot;&amp;gt;
                &amp;lt;h1&amp;gt;MyApp에 오신 것을 환영합니다!&amp;lt;/h1&amp;gt;
                &amp;lt;p&amp;gt;우리는 최고의 서비스를 제공합니다.&amp;lt;/p&amp;gt;

                &amp;lt;div class=&quot;featured-products&quot;&amp;gt;
                    &amp;lt;h2&amp;gt;추천 상품&amp;lt;/h2&amp;gt;
                    &amp;lt;div class=&quot;product-grid&quot;&amp;gt;
                        &amp;lt;div th:each=&quot;product : ${featuredProducts}&quot; class=&quot;product-card&quot;&amp;gt;
                            &amp;lt;img th:src=&quot;@{/images/products/{id}.jpg(id=${product.id})}&quot; 
                                 th:alt=&quot;${product.name}&quot;&amp;gt;
                            &amp;lt;h3 th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/h3&amp;gt;
                            &amp;lt;p th:text=&quot;${#numbers.formatCurrency(product.price)}&quot;&amp;gt;가격&amp;lt;/p&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/main&amp;gt;

            &amp;lt;!-- 사이드바 프래그먼트 삽입 --&amp;gt;
            &amp;lt;div class=&quot;col-md-3&quot;&amp;gt;
                &amp;lt;div th:replace=&quot;~{fragments/common :: sidebar}&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 푸터 프래그먼트 삽입 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: footer}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프래그먼트 참조 문법:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;~{fragments/common :: header}&lt;/code&gt;: &lt;code&gt;fragments/common.html&lt;/code&gt;의 &lt;code&gt;header&lt;/code&gt; 프래그먼트&lt;/li&gt;
&lt;li&gt;경로는 &lt;code&gt;templates&lt;/code&gt; 폴더 기준의 상대 경로&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::&lt;/code&gt; 뒤에 프래그먼트 이름 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 프래그먼트 삽입 (&lt;code&gt;th:insert&lt;/code&gt;, &lt;code&gt;th:replace&lt;/code&gt;, &lt;code&gt;th:include&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프래그먼트를 삽입하는 방법에는 여러 가지가 있으며, 각각 다른 결과를 만들어냅니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;삽입 방식의 차이점&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 예시용 프래그먼트를 정의해보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/fragments/sample.html --&amp;gt;
&amp;lt;div th:fragment=&quot;myContent&quot; class=&quot;my-fragment&quot;&amp;gt;
    &amp;lt;h2&amp;gt;프래그먼트 제목&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 세 가지 삽입 방식을 비교해보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/test.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;프래그먼트 삽입 테스트&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!-- 1. th:insert 사용 --&amp;gt;
    &amp;lt;section class=&quot;test-section&quot;&amp;gt;
        &amp;lt;h1&amp;gt;th:insert 결과&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&quot;host-container&quot; th:insert=&quot;~{fragments/sample :: myContent}&quot;&amp;gt;
            &amp;lt;p&amp;gt;기존 내용입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;

    &amp;lt;!-- 2. th:replace 사용 --&amp;gt;
    &amp;lt;section class=&quot;test-section&quot;&amp;gt;
        &amp;lt;h1&amp;gt;th:replace 결과&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&quot;host-container&quot; th:replace=&quot;~{fragments/sample :: myContent}&quot;&amp;gt;
            &amp;lt;p&amp;gt;이 내용은 완전히 사라집니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;

    &amp;lt;!-- 3. th:include 사용 (deprecated, 사용 권장하지 않음) --&amp;gt;
    &amp;lt;section class=&quot;test-section&quot;&amp;gt;
        &amp;lt;h1&amp;gt;th:include 결과&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&quot;host-container&quot; th:include=&quot;~{fragments/sample :: myContent}&quot;&amp;gt;
            &amp;lt;p&amp;gt;이 내용도 사라집니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;렌더링 결과:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;!-- 1. th:insert 결과 --&amp;gt;
    &amp;lt;section class=&quot;test-section&quot;&amp;gt;
        &amp;lt;h1&amp;gt;th:insert 결과&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&quot;host-container&quot;&amp;gt;
            &amp;lt;p&amp;gt;기존 내용입니다.&amp;lt;/p&amp;gt;
            &amp;lt;div class=&quot;my-fragment&quot;&amp;gt;
                &amp;lt;h2&amp;gt;프래그먼트 제목&amp;lt;/h2&amp;gt;
                &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;

    &amp;lt;!-- 2. th:replace 결과 --&amp;gt;
    &amp;lt;section class=&quot;test-section&quot;&amp;gt;
        &amp;lt;h1&amp;gt;th:replace 결과&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&quot;my-fragment&quot;&amp;gt;
            &amp;lt;h2&amp;gt;프래그먼트 제목&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;

    &amp;lt;!-- 3. th:include 결과 --&amp;gt;
    &amp;lt;section class=&quot;test-section&quot;&amp;gt;
        &amp;lt;h1&amp;gt;th:include 결과&amp;lt;/h1&amp;gt;
        &amp;lt;div class=&quot;host-container&quot;&amp;gt;
            &amp;lt;h2&amp;gt;프래그먼트 제목&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/section&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;언제 어떤 방식을 사용할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;th:replace&lt;/code&gt; (가장 일반적, 권장)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호스트 태그를 프래그먼트로 완전히 교체&lt;/li&gt;
&lt;li&gt;대부분의 상황에서 가장 직관적이고 깔끔&lt;/li&gt;
&lt;li&gt;헤더, 푸터, 네비게이션 등에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;th:insert&lt;/code&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 태그를 유지하면서 내부에 프래그먼트 추가&lt;/li&gt;
&lt;li&gt;호스트 태그의 CSS 클래스나 속성을 유지해야 할 때&lt;/li&gt;
&lt;li&gt;컨테이너 역할을 하는 태그가 중요할 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;th:include&lt;/code&gt; (사용 권장하지 않음)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Thymeleaf 3.0부터 deprecated&lt;/li&gt;
&lt;li&gt;프래그먼트의 내용만 가져오고 태그는 제외&lt;/li&gt;
&lt;li&gt;&lt;code&gt;th:insert&lt;/code&gt;나 &lt;code&gt;th:replace&lt;/code&gt; 사용 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;실제 활용 예제&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/fragments/alerts.html --&amp;gt;
&amp;lt;!-- 성공 알림 프래그먼트 --&amp;gt;
&amp;lt;div th:fragment=&quot;success(message)&quot; class=&quot;alert alert-success&quot; role=&quot;alert&quot;&amp;gt;
    &amp;lt;i class=&quot;fas fa-check-circle&quot;&amp;gt;&amp;lt;/i&amp;gt;
    &amp;lt;strong&amp;gt;성공!&amp;lt;/strong&amp;gt; &amp;lt;span th:text=&quot;${message}&quot;&amp;gt;작업이 완료되었습니다.&amp;lt;/span&amp;gt;
    &amp;lt;button type=&quot;button&quot; class=&quot;btn-close&quot; data-bs-dismiss=&quot;alert&quot;&amp;gt;&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 오류 알림 프래그먼트 --&amp;gt;
&amp;lt;div th:fragment=&quot;error(message)&quot; class=&quot;alert alert-danger&quot; role=&quot;alert&quot;&amp;gt;
    &amp;lt;i class=&quot;fas fa-exclamation-triangle&quot;&amp;gt;&amp;lt;/i&amp;gt;
    &amp;lt;strong&amp;gt;오류!&amp;lt;/strong&amp;gt; &amp;lt;span th:text=&quot;${message}&quot;&amp;gt;문제가 발생했습니다.&amp;lt;/span&amp;gt;
    &amp;lt;button type=&quot;button&quot; class=&quot;btn-close&quot; data-bs-dismiss=&quot;alert&quot;&amp;gt;&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 경고 알림 프래그먼트 --&amp;gt;
&amp;lt;div th:fragment=&quot;warning(message)&quot; class=&quot;alert alert-warning&quot; role=&quot;alert&quot;&amp;gt;
    &amp;lt;i class=&quot;fas fa-exclamation-circle&quot;&amp;gt;&amp;lt;/i&amp;gt;
    &amp;lt;strong&amp;gt;주의!&amp;lt;/strong&amp;gt; &amp;lt;span th:text=&quot;${message}&quot;&amp;gt;주의가 필요합니다.&amp;lt;/span&amp;gt;
    &amp;lt;button type=&quot;button&quot; class=&quot;btn-close&quot; data-bs-dismiss=&quot;alert&quot;&amp;gt;&amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 알림 프래그먼트 사용 --&amp;gt;
&amp;lt;div class=&quot;alerts-container&quot;&amp;gt;
    &amp;lt;!-- 성공 메시지가 있을 때만 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${successMessage}&quot; 
         th:replace=&quot;~{fragments/alerts :: success(${successMessage})}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- 오류 메시지가 있을 때만 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${errorMessage}&quot; 
         th:replace=&quot;~{fragments/alerts :: error(${errorMessage})}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- 경고 메시지가 있을 때만 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${warningMessage}&quot; 
         th:replace=&quot;~{fragments/alerts :: warning(${warningMessage})}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.3 레이아웃 상속과 확장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이아웃 상속을 통해 공통 구조를 정의하고, 각 페이지에서 필요한 부분만 변경할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 레이아웃 템플릿&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- templates/layouts/base.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;

    &amp;lt;!-- 동적 제목 설정 --&amp;gt;
    &amp;lt;title th:text=&quot;${pageTitle != null} ? ${pageTitle} + ' - MyApp' : 'MyApp'&quot;&amp;gt;MyApp&amp;lt;/title&amp;gt;

    &amp;lt;!-- 메타 태그 --&amp;gt;
    &amp;lt;meta name=&quot;description&quot; th:content=&quot;${pageDescription ?: 'MyApp 기본 설명'}&quot;&amp;gt;
    &amp;lt;meta name=&quot;keywords&quot; th:content=&quot;${pageKeywords ?: '기본, 키워드'}&quot;&amp;gt;

    &amp;lt;!-- 기본 CSS --&amp;gt;
    &amp;lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;link href=&quot;https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/main.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;

    &amp;lt;!-- 페이지별 추가 CSS (하위 템플릿에서 정의) --&amp;gt;
    &amp;lt;th:block th:fragment=&quot;extra-css&quot;&amp;gt;
        &amp;lt;!-- 여기에 페이지별 CSS가 추가됩니다 --&amp;gt;
    &amp;lt;/th:block&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body th:class=&quot;${bodyClass ?: ''}&quot;&amp;gt;

    &amp;lt;!-- 헤더 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: header}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- 브레드크럼 네비게이션 (선택적) --&amp;gt;
    &amp;lt;nav th:if=&quot;${breadcrumbs}&quot; aria-label=&quot;breadcrumb&quot; class=&quot;breadcrumb-nav&quot;&amp;gt;
        &amp;lt;div class=&quot;container&quot;&amp;gt;
            &amp;lt;ol class=&quot;breadcrumb&quot;&amp;gt;
                &amp;lt;li class=&quot;breadcrumb-item&quot;&amp;gt;
                    &amp;lt;a th:href=&quot;@{/}&quot;&amp;gt;&amp;lt;i class=&quot;fas fa-home&quot;&amp;gt;&amp;lt;/i&amp;gt; 홈&amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;
                &amp;lt;li th:each=&quot;crumb, stat : ${breadcrumbs}&quot;
                    th:class=&quot;${stat.last} ? 'breadcrumb-item active' : 'breadcrumb-item'&quot;&amp;gt;
                    &amp;lt;a th:unless=&quot;${stat.last}&quot; 
                       th:href=&quot;@{${crumb.url}}&quot; 
                       th:text=&quot;${crumb.title}&quot;&amp;gt;브레드크럼&amp;lt;/a&amp;gt;
                    &amp;lt;span th:if=&quot;${stat.last}&quot; th:text=&quot;${crumb.title}&quot;&amp;gt;현재 페이지&amp;lt;/span&amp;gt;
                &amp;lt;/li&amp;gt;
            &amp;lt;/ol&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/nav&amp;gt;

    &amp;lt;!-- 메인 콘텐츠 영역 --&amp;gt;
    &amp;lt;main class=&quot;main-content&quot;&amp;gt;
        &amp;lt;div class=&quot;container&quot;&amp;gt;

            &amp;lt;!-- 페이지 헤더 --&amp;gt;
            &amp;lt;div th:if=&quot;${pageTitle}&quot; class=&quot;page-header mb-4&quot;&amp;gt;
                &amp;lt;div class=&quot;row align-items-center&quot;&amp;gt;
                    &amp;lt;div class=&quot;col&quot;&amp;gt;
                        &amp;lt;h1 th:text=&quot;${pageTitle}&quot;&amp;gt;페이지 제목&amp;lt;/h1&amp;gt;
                        &amp;lt;p th:if=&quot;${pageSubtitle}&quot; 
                           th:text=&quot;${pageSubtitle}&quot; 
                           class=&quot;text-muted&quot;&amp;gt;페이지 부제목&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div th:if=&quot;${pageActions}&quot; class=&quot;col-auto&quot;&amp;gt;
                        &amp;lt;div class=&quot;page-actions&quot;&amp;gt;
                            &amp;lt;a th:each=&quot;action : ${pageActions}&quot;
                               th:href=&quot;@{${action.url}}&quot;
                               th:class=&quot;'btn ' + ${action.btnClass}&quot;
                               th:text=&quot;${action.label}&quot;&amp;gt;액션&amp;lt;/a&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 알림 메시지 --&amp;gt;
            &amp;lt;div class=&quot;alerts&quot;&amp;gt;
                &amp;lt;div th:if=&quot;${successMessage}&quot; 
                     th:replace=&quot;~{fragments/alerts :: success(${successMessage})}&quot;&amp;gt;&amp;lt;/div&amp;gt;
                &amp;lt;div th:if=&quot;${errorMessage}&quot; 
                     th:replace=&quot;~{fragments/alerts :: error(${errorMessage})}&quot;&amp;gt;&amp;lt;/div&amp;gt;
                &amp;lt;div th:if=&quot;${warningMessage}&quot; 
                     th:replace=&quot;~{fragments/alerts :: warning(${warningMessage})}&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 메인 콘텐츠 블록 (하위 템플릿에서 구현) --&amp;gt;
            &amp;lt;div th:fragment=&quot;content&quot; class=&quot;page-content&quot;&amp;gt;
                &amp;lt;!-- 기본 콘텐츠 - 하위 템플릿에서 오버라이드됩니다 --&amp;gt;
                &amp;lt;div class=&quot;alert alert-info&quot;&amp;gt;
                    &amp;lt;p&amp;gt;이 영역은 하위 템플릿에서 정의해야 합니다.&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;

    &amp;lt;!-- 푸터 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: footer}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- 기본 JavaScript --&amp;gt;
    &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script th:src=&quot;@{/js/main.js}&quot;&amp;gt;&amp;lt;/script&amp;gt;

    &amp;lt;!-- 페이지별 추가 JavaScript (하위 템플릿에서 정의) --&amp;gt;
    &amp;lt;th:block th:fragment=&quot;extra-js&quot;&amp;gt;
        &amp;lt;!-- 여기에 페이지별 JavaScript가 추가됩니다 --&amp;gt;
    &amp;lt;/th:block&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;레이아웃을 상속하는 페이지 예제&lt;/u&gt;&lt;/h4&gt;
&lt;!-- templates/products/list.html --&gt;&lt;!-- 베이스 레이아웃의 head가 여기에 들어옵니다 --&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 추가 CSS --&amp;gt;
&amp;lt;th:block th:fragment=&quot;extra-css&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/products.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        .product-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/th:block&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;!-- 베이스 레이아웃의 헤더, 브레드크럼, 알림 등이 자동으로 포함됩니다 --&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 브레드크럼 설정 --&amp;gt;
&amp;lt;div th:with=&quot;breadcrumbs=${
    {
        {title: '상품', url: '/products'}
    }
}&quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;!-- 페이지 액션 설정 --&amp;gt;
&amp;lt;div th:with=&quot;pageActions=${
    {
        {label: '상품 추가', url: '/products/new', btnClass: 'btn-primary'}
    }
}&quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;!-- 메인 콘텐츠 - base.html의 content 블록을 오버라이드 --&amp;gt;
&amp;lt;div th:fragment=&quot;content&quot; class=&quot;page-content&quot; th:remove=&quot;tag&quot;&amp;gt;

    &amp;lt;!-- 필터 섹션 --&amp;gt;
    &amp;lt;div class=&quot;filters card mb-4&quot;&amp;gt;
        &amp;lt;div class=&quot;card-body&quot;&amp;gt;
            &amp;lt;h5 class=&quot;card-title&quot;&amp;gt;필터&amp;lt;/h5&amp;gt;
            &amp;lt;form th:action=&quot;@{/products}&quot; method=&quot;get&quot; class=&quot;row g-3&quot;&amp;gt;

                &amp;lt;div class=&quot;col-md-3&quot;&amp;gt;
                    &amp;lt;label for=&quot;category&quot; class=&quot;form-label&quot;&amp;gt;카테고리&amp;lt;/label&amp;gt;
                    &amp;lt;select name=&quot;category&quot; id=&quot;category&quot; class=&quot;form-select&quot;&amp;gt;
                        &amp;lt;option value=&quot;&quot;&amp;gt;전체 카테고리&amp;lt;/option&amp;gt;
                        &amp;lt;option th:each=&quot;cat : ${categories}&quot; 
                                th:value=&quot;${cat.id}&quot; 
                                th:text=&quot;${cat.name}&quot;
                                th:selected=&quot;${cat.id == selectedCategory}&quot;&amp;gt;카테고리&amp;lt;/option&amp;gt;
                    &amp;lt;/select&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&quot;col-md-3&quot;&amp;gt;
                    &amp;lt;label for=&quot;minPrice&quot; class=&quot;form-label&quot;&amp;gt;최소 가격&amp;lt;/label&amp;gt;
                    &amp;lt;input type=&quot;number&quot; name=&quot;minPrice&quot; id=&quot;minPrice&quot; 
                           th:value=&quot;${minPrice}&quot; class=&quot;form-control&quot; 
                           placeholder=&quot;0&quot;&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&quot;col-md-3&quot;&amp;gt;
                    &amp;lt;label for=&quot;maxPrice&quot; class=&quot;form-label&quot;&amp;gt;최대 가격&amp;lt;/label&amp;gt;
                    &amp;lt;input type=&quot;number&quot; name=&quot;maxPrice&quot; id=&quot;maxPrice&quot; 
                           th:value=&quot;${maxPrice}&quot; class=&quot;form-control&quot; 
                           placeholder=&quot;무제한&quot;&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&quot;col-md-3 d-flex align-items-end&quot;&amp;gt;
                    &amp;lt;button type=&quot;submit&quot; class=&quot;btn btn-primary me-2&quot;&amp;gt;필터 적용&amp;lt;/button&amp;gt;
                    &amp;lt;a th:href=&quot;@{/products}&quot; class=&quot;btn btn-outline-secondary&quot;&amp;gt;초기화&amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/form&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 상품 목록 --&amp;gt;
    &amp;lt;div th:if=&quot;${#lists.isEmpty(products)}&quot; class=&quot;alert alert-info&quot;&amp;gt;
        &amp;lt;h4&amp;gt;상품이 없습니다&amp;lt;/h4&amp;gt;
        &amp;lt;p&amp;gt;조건에 맞는 상품이 없습니다. 필터를 조정해보세요.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${#lists.isEmpty(products)}&quot;&amp;gt;
        &amp;lt;!-- 정렬 옵션 --&amp;gt;
        &amp;lt;div class=&quot;d-flex justify-content-between align-items-center mb-3&quot;&amp;gt;
            &amp;lt;p class=&quot;text-muted mb-0&quot;&amp;gt;
                총 &amp;lt;strong th:text=&quot;${totalProducts}&quot;&amp;gt;0&amp;lt;/strong&amp;gt;개의 상품이 있습니다.
            &amp;lt;/p&amp;gt;

            &amp;lt;div class=&quot;sort-options&quot;&amp;gt;
                &amp;lt;select name=&quot;sort&quot; class=&quot;form-select form-select-sm&quot; style=&quot;width: auto;&quot; onchange=&quot;location = this.value;&quot;&amp;gt;
                    &amp;lt;option th:value=&quot;@{/products(sort='name')}&quot; 
                            th:selected=&quot;${sort == 'name'}&quot;&amp;gt;이름순&amp;lt;/option&amp;gt;
                    &amp;lt;option th:value=&quot;@{/products(sort='price-low')}&quot; 
                            th:selected=&quot;${sort == 'price-low'}&quot;&amp;gt;가격 낮은순&amp;lt;/option&amp;gt;
                    &amp;lt;option th:value=&quot;@{/products(sort='price-high')}&quot; 
                            th:selected=&quot;${sort == 'price-high'}&quot;&amp;gt;가격 높은순&amp;lt;/option&amp;gt;
                    &amp;lt;option th:value=&quot;@{/products(sort='newest')}&quot; 
                            th:selected=&quot;${sort == 'newest'}&quot;&amp;gt;최신순&amp;lt;/option&amp;gt;
                &amp;lt;/select&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 상품 그리드 --&amp;gt;
        &amp;lt;div class=&quot;product-grid&quot;&amp;gt;
            &amp;lt;div th:each=&quot;product : ${products}&quot; class=&quot;card product-card&quot;&amp;gt;
                &amp;lt;img th:src=&quot;@{/images/products/{id}.jpg(id=${product.id})}&quot; 
                     th:alt=&quot;${product.name}&quot; class=&quot;card-img-top&quot;&amp;gt;

                &amp;lt;div class=&quot;card-body&quot;&amp;gt;
                    &amp;lt;h5 class=&quot;card-title&quot; th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/h5&amp;gt;
                    &amp;lt;p class=&quot;card-text&quot; th:text=&quot;${#strings.abbreviate(product.description, 80)}&quot;&amp;gt;
                        상품 설명
                    &amp;lt;/p&amp;gt;

                    &amp;lt;div class=&quot;price-section&quot;&amp;gt;
                        &amp;lt;span th:if=&quot;${product.originalPrice != product.currentPrice}&quot; 
                              class=&quot;text-muted text-decoration-line-through small&quot;
                              th:text=&quot;${#numbers.formatCurrency(product.originalPrice)}&quot;&amp;gt;₩129,000&amp;lt;/span&amp;gt;
                        &amp;lt;span class=&quot;h5 text-primary&quot; 
                              th:text=&quot;${#numbers.formatCurrency(product.currentPrice)}&quot;&amp;gt;₩99,000&amp;lt;/span&amp;gt;
                        &amp;lt;span th:if=&quot;${product.discountRate &amp;gt; 0}&quot; 
                              class=&quot;badge bg-danger ms-2&quot;
                              th:text=&quot;${product.discountRate} + '%'&quot;&amp;gt;20%&amp;lt;/span&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class=&quot;d-grid gap-2 d-md-flex justify-content-md-end mt-3&quot;&amp;gt;
                        &amp;lt;a th:href=&quot;@{/products/{id}(id=${product.id})}&quot; 
                           class=&quot;btn btn-outline-primary btn-sm&quot;&amp;gt;상세보기&amp;lt;/a&amp;gt;
                        &amp;lt;button class=&quot;btn btn-primary btn-sm&quot; 
                                th:onclick=&quot;|addToCart(${product.id})|&quot;&amp;gt;
                            &amp;lt;i class=&quot;fas fa-cart-plus&quot;&amp;gt;&amp;lt;/i&amp;gt; 장바구니
                        &amp;lt;/button&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 페이지네이션 --&amp;gt;
        &amp;lt;nav th:if=&quot;${totalPages &amp;gt; 1}&quot; aria-label=&quot;상품 페이지네이션&quot; class=&quot;mt-4&quot;&amp;gt;
            &amp;lt;ul class=&quot;pagination justify-content-center&quot;&amp;gt;

                &amp;lt;!-- 이전 페이지 --&amp;gt;
                &amp;lt;li th:class=&quot;${currentPage == 0} ? 'page-item disabled' : 'page-item'&quot;&amp;gt;
                    &amp;lt;a class=&quot;page-link&quot; 
                       th:href=&quot;@{/products(page=${currentPage - 1}, category=${selectedCategory}, minPrice=${minPrice}, maxPrice=${maxPrice}, sort=${sort})}&quot;
                       th:unless=&quot;${currentPage == 0}&quot;&amp;gt;이전&amp;lt;/a&amp;gt;
                    &amp;lt;span th:if=&quot;${currentPage == 0}&quot; class=&quot;page-link&quot;&amp;gt;이전&amp;lt;/span&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;!-- 페이지 번호들 --&amp;gt;
                &amp;lt;li th:each=&quot;pageNum : ${#numbers.sequence(0, totalPages - 1)}&quot;
                    th:class=&quot;${pageNum == currentPage} ? 'page-item active' : 'page-item'&quot;&amp;gt;
                    &amp;lt;a class=&quot;page-link&quot; 
                       th:href=&quot;@{/products(page=${pageNum}, category=${selectedCategory}, minPrice=${minPrice}, maxPrice=${maxPrice}, sort=${sort})}&quot;
                       th:text=&quot;${pageNum + 1}&quot;&amp;gt;1&amp;lt;/a&amp;gt;
                &amp;lt;/li&amp;gt;

                &amp;lt;!-- 다음 페이지 --&amp;gt;
                &amp;lt;li th:class=&quot;${currentPage &amp;gt;= totalPages - 1} ? 'page-item disabled' : 'page-item'&quot;&amp;gt;
                    &amp;lt;a class=&quot;page-link&quot; 
                       th:href=&quot;@{/products(page=${currentPage + 1}, category=${selectedCategory}, minPrice=${minPrice}, maxPrice=${maxPrice}, sort=${sort})}&quot;
                       th:unless=&quot;${currentPage &amp;gt;= totalPages - 1}&quot;&amp;gt;다음&amp;lt;/a&amp;gt;
                    &amp;lt;span th:if=&quot;${currentPage &amp;gt;= totalPages - 1}&quot; class=&quot;page-link&quot;&amp;gt;다음&amp;lt;/span&amp;gt;
                &amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/nav&amp;gt;
    &amp;lt;/div&amp;gt;

&amp;lt;/div&amp;gt;

&amp;lt;!-- 추가 JavaScript --&amp;gt;
&amp;lt;th:block th:fragment=&quot;extra-js&quot;&amp;gt;
    &amp;lt;script&amp;gt;
        function addToCart(productId)&lt;/code&gt;&lt;/pre&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/145</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-6%EC%9E%A5-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83%EA%B3%BC-%ED%94%84%EB%9E%98%EA%B7%B8%EB%A8%BC%ED%8A%B8#entry145comment</comments>
      <pubDate>Tue, 26 Aug 2025 10:43:18 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #5. 표준속성 (2)</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-5-%ED%91%9C%EC%A4%80%EC%86%8D%EC%84%B1-2</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Mx1I/btsP4ucxYeS/gMr5kP0z1QGWkTqSlX0wX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Mx1I/btsP4ucxYeS/gMr5kP0z1QGWkTqSlX0wX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Mx1I/btsP4ucxYeS/gMr5kP0z1QGWkTqSlX0wX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Mx1I%2FbtsP4ucxYeS%2FgMr5kP0z1QGWkTqSlX0wX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.4 반복 처리 (&lt;code&gt;th:each&lt;/code&gt;)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 반복 처리&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;basic-iteration&quot;&amp;gt;
    &amp;lt;!-- 리스트 반복 --&amp;gt;
    &amp;lt;div class=&quot;user-list&quot;&amp;gt;
        &amp;lt;div th:each=&quot;user : ${users}&quot; class=&quot;user-card&quot;&amp;gt;
            &amp;lt;h3 th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/h3&amp;gt;
            &amp;lt;p th:text=&quot;${user.email}&quot;&amp;gt;이메일&amp;lt;/p&amp;gt;
            &amp;lt;span th:text=&quot;${user.role}&quot;&amp;gt;역할&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 배열 반복 --&amp;gt;
    &amp;lt;ul class=&quot;tag-list&quot;&amp;gt;
        &amp;lt;li th:each=&quot;tag : ${post.tags}&quot; th:text=&quot;${tag}&quot;&amp;gt;태그&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;

    &amp;lt;!-- 맵 반복 --&amp;gt;
    &amp;lt;div class=&quot;settings&quot;&amp;gt;
        &amp;lt;div th:each=&quot;setting : ${userSettings}&quot; class=&quot;setting-item&quot;&amp;gt;
            &amp;lt;label th:text=&quot;${setting.key}&quot;&amp;gt;설정명&amp;lt;/label&amp;gt;
            &amp;lt;span th:text=&quot;${setting.value}&quot;&amp;gt;설정값&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 반복 (각 문자) --&amp;gt;
    &amp;lt;div class=&quot;char-display&quot;&amp;gt;
        &amp;lt;span th:each=&quot;char : ${word}&quot; th:text=&quot;${char}&quot;&amp;gt;문자&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;상태 변수 활용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;iteration-status&quot;&amp;gt;
    &amp;lt;!-- 기본 상태 변수 --&amp;gt;
    &amp;lt;table class=&quot;data-table&quot;&amp;gt;
        &amp;lt;thead&amp;gt;
            &amp;lt;tr&amp;gt;
                &amp;lt;th&amp;gt;순번&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;이름&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;이메일&amp;lt;/th&amp;gt;
                &amp;lt;th&amp;gt;상태&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
        &amp;lt;/thead&amp;gt;
        &amp;lt;tbody&amp;gt;
            &amp;lt;tr th:each=&quot;user, stat : ${users}&quot; 
                th:class=&quot;${stat.odd} ? 'odd-row' : 'even-row'&quot;&amp;gt;
                &amp;lt;td th:text=&quot;${stat.count}&quot;&amp;gt;1&amp;lt;/td&amp;gt;
                &amp;lt;td th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/td&amp;gt;
                &amp;lt;td th:text=&quot;${user.email}&quot;&amp;gt;이메일&amp;lt;/td&amp;gt;
                &amp;lt;td&amp;gt;
                    &amp;lt;span th:if=&quot;${stat.first}&quot; class=&quot;badge&quot;&amp;gt;신규&amp;lt;/span&amp;gt;
                    &amp;lt;span th:if=&quot;${stat.last}&quot; class=&quot;badge&quot;&amp;gt;마지막&amp;lt;/span&amp;gt;
                &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
        &amp;lt;/tbody&amp;gt;
    &amp;lt;/table&amp;gt;

    &amp;lt;!-- 상세한 상태 정보 활용 --&amp;gt;
    &amp;lt;div class=&quot;product-grid&quot;&amp;gt;
        &amp;lt;div th:each=&quot;product, status : ${products}&quot; 
             class=&quot;product-card&quot;
             th:classappend=&quot;${status.first} ? 'first-item' : ''&quot;
             th:attr=&quot;data-index=${status.index},
                     data-count=${status.count},
                     data-total=${status.size}&quot;&amp;gt;

            &amp;lt;span class=&quot;item-number&quot; th:text=&quot;${status.count} + '/' + ${status.size}&quot;&amp;gt;1/10&amp;lt;/span&amp;gt;
            &amp;lt;h3 th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/h3&amp;gt;

            &amp;lt;!-- 첫 번째와 마지막 아이템 특별 처리 --&amp;gt;
            &amp;lt;div th:if=&quot;${status.first}&quot; class=&quot;featured-badge&quot;&amp;gt;추천&amp;lt;/div&amp;gt;
            &amp;lt;div th:if=&quot;${status.last}&quot; class=&quot;last-chance&quot;&amp;gt;마지막 기회!&amp;lt;/div&amp;gt;

            &amp;lt;!-- 홀짝 번째 아이템 다른 스타일 --&amp;gt;
            &amp;lt;div th:class=&quot;${status.even} ? 'price-highlight' : 'price-normal'&quot;&amp;gt;
                &amp;lt;span th:text=&quot;${#numbers.formatCurrency(product.price)}&quot;&amp;gt;₩10,000&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 페이지네이션 정보와 함께 --&amp;gt;
    &amp;lt;div class=&quot;pagination-info&quot; th:if=&quot;${not #lists.isEmpty(items)}&quot;&amp;gt;
        &amp;lt;p&amp;gt;
            전체 &amp;lt;span th:text=&quot;${totalItems}&quot;&amp;gt;100&amp;lt;/span&amp;gt;개 중 
            &amp;lt;span th:text=&quot;${(currentPage * pageSize) + 1}&quot;&amp;gt;1&amp;lt;/span&amp;gt;-
            &amp;lt;span th:text=&quot;${#numbers.min((currentPage + 1) * pageSize, totalItems)}&quot;&amp;gt;20&amp;lt;/span&amp;gt;
            항목 표시
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;중첩 반복과 복잡한 구조&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;nested-iteration&quot;&amp;gt;
    &amp;lt;!-- 카테고리별 상품 목록 --&amp;gt;
    &amp;lt;div th:each=&quot;category : ${categories}&quot; class=&quot;category-section&quot;&amp;gt;
        &amp;lt;h2 th:text=&quot;${category.name}&quot;&amp;gt;카테고리명&amp;lt;/h2&amp;gt;

        &amp;lt;div class=&quot;products-grid&quot;&amp;gt;
            &amp;lt;div th:each=&quot;product, productStat : ${category.products}&quot; class=&quot;product-item&quot;&amp;gt;
                &amp;lt;span class=&quot;category-product-number&quot; 
                      th:text=&quot;${category.name} + '-' + ${productStat.count}&quot;&amp;gt;CAT-1&amp;lt;/span&amp;gt;
                &amp;lt;h3 th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/h3&amp;gt;

                &amp;lt;!-- 상품별 리뷰 목록 --&amp;gt;
                &amp;lt;div th:if=&quot;${not #lists.isEmpty(product.reviews)}&quot; class=&quot;reviews&quot;&amp;gt;
                    &amp;lt;h4&amp;gt;리뷰 (&amp;lt;span th:text=&quot;${#lists.size(product.reviews)}&quot;&amp;gt;3&amp;lt;/span&amp;gt;개)&amp;lt;/h4&amp;gt;
                    &amp;lt;div th:each=&quot;review, reviewStat : ${product.reviews}&quot; 
                         th:if=&quot;${reviewStat.index &amp;lt; 3}&quot; class=&quot;review-item&quot;&amp;gt;
                        &amp;lt;div class=&quot;review-header&quot;&amp;gt;
                            &amp;lt;span th:text=&quot;${review.author}&quot;&amp;gt;작성자&amp;lt;/span&amp;gt;
                            &amp;lt;div class=&quot;stars&quot;&amp;gt;
                                &amp;lt;span th:each=&quot;star : ${#numbers.sequence(1, 5)}&quot;
                                      th:class=&quot;${star &amp;lt;= review.rating} ? 'star filled' : 'star empty'&quot;&amp;gt;★&amp;lt;/span&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;lt;/div&amp;gt;
                        &amp;lt;p th:text=&quot;${review.content}&quot;&amp;gt;리뷰 내용&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;a th:if=&quot;${#lists.size(product.reviews) &amp;gt; 3}&quot; 
                       th:href=&quot;@{/product/{id}/reviews(id=${product.id})}&quot;&amp;gt;
                        더 보기 (+&amp;lt;span th:text=&quot;${#lists.size(product.reviews) - 3}&quot;&amp;gt;2&amp;lt;/span&amp;gt;개)
                    &amp;lt;/a&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 월별 통계 데이터 --&amp;gt;
    &amp;lt;div class=&quot;monthly-stats&quot;&amp;gt;
        &amp;lt;table class=&quot;stats-table&quot;&amp;gt;
            &amp;lt;thead&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;th&amp;gt;월&amp;lt;/th&amp;gt;
                    &amp;lt;th th:each=&quot;metric : ${metrics}&quot; th:text=&quot;${metric.name}&quot;&amp;gt;지표&amp;lt;/th&amp;gt;
                &amp;lt;/tr&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;
                &amp;lt;tr th:each=&quot;month : ${monthlyData}&quot;&amp;gt;
                    &amp;lt;td th:text=&quot;${#dates.format(month.date, 'yyyy-MM')}&quot;&amp;gt;2024-01&amp;lt;/td&amp;gt;
                    &amp;lt;td th:each=&quot;metric : ${metrics}&quot; 
                        th:text=&quot;${month.values[metric.key]}&quot;&amp;gt;100&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
            &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 트리 구조 (재귀적 렌더링) --&amp;gt;
    &amp;lt;div class=&quot;menu-tree&quot;&amp;gt;
        &amp;lt;ul&amp;gt;
            &amp;lt;li th:each=&quot;menuItem : ${menuItems}&quot;&amp;gt;
                &amp;lt;span th:text=&quot;${menuItem.name}&quot;&amp;gt;메뉴명&amp;lt;/span&amp;gt;
                &amp;lt;!-- 하위 메뉴가 있는 경우 --&amp;gt;
                &amp;lt;ul th:if=&quot;${not #lists.isEmpty(menuItem.children)}&quot;&amp;gt;
                    &amp;lt;li th:each=&quot;childItem : ${menuItem.children}&quot;&amp;gt;
                        &amp;lt;span th:text=&quot;${childItem.name}&quot;&amp;gt;하위메뉴&amp;lt;/span&amp;gt;
                        &amp;lt;!-- 3단계 메뉴 --&amp;gt;
                        &amp;lt;ul th:if=&quot;${not #lists.isEmpty(childItem.children)}&quot;&amp;gt;
                            &amp;lt;li th:each=&quot;grandChild : ${childItem.children}&quot;&amp;gt;
                                &amp;lt;span th:text=&quot;${grandChild.name}&quot;&amp;gt;3단계메뉴&amp;lt;/span&amp;gt;
                            &amp;lt;/li&amp;gt;
                        &amp;lt;/ul&amp;gt;
                    &amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;
            &amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;조건부 반복과 필터링&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-iteration&quot;&amp;gt;
    &amp;lt;!-- 조건에 맞는 항목만 표시 --&amp;gt;
    &amp;lt;div class=&quot;active-users&quot;&amp;gt;
        &amp;lt;h3&amp;gt;활성 사용자&amp;lt;/h3&amp;gt;
        &amp;lt;div th:each=&quot;user : ${users}&quot; th:if=&quot;${user.isActive()}&quot; class=&quot;user-card&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
            &amp;lt;span class=&quot;status active&quot;&amp;gt;활성&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 컬렉션 선택을 통한 필터링 --&amp;gt;
    &amp;lt;div class=&quot;premium-users&quot;&amp;gt;
        &amp;lt;h3&amp;gt;프리미엄 사용자&amp;lt;/h3&amp;gt;
        &amp;lt;div th:each=&quot;user : ${users.?[isPremium()]}&quot; class=&quot;premium-user-card&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;프리미엄 사용자&amp;lt;/span&amp;gt;
            &amp;lt;span class=&quot;badge gold&quot;&amp;gt;PREMIUM&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 점수별 필터링 --&amp;gt;
    &amp;lt;div class=&quot;high-score-users&quot;&amp;gt;
        &amp;lt;h3&amp;gt;고득점 사용자 (80점 이상)&amp;lt;/h3&amp;gt;
        &amp;lt;div th:each=&quot;user : ${users.?[score &amp;gt;= 80]}&quot; class=&quot;high-score-card&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
            &amp;lt;span th:text=&quot;${user.score}&quot;&amp;gt;점수&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 복합 조건 필터링 --&amp;gt;
    &amp;lt;div class=&quot;qualified-users&quot;&amp;gt;
        &amp;lt;h3&amp;gt;자격 요건 충족 사용자&amp;lt;/h3&amp;gt;
        &amp;lt;div th:each=&quot;user : ${users.?[isActive() and isVerified() and age &amp;gt;= 18]}&quot; 
             class=&quot;qualified-user&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
            &amp;lt;span class=&quot;qualification-badge&quot;&amp;gt;✓ 자격충족&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 빈 목록 처리 --&amp;gt;
    &amp;lt;div class=&quot;user-notifications&quot;&amp;gt;
        &amp;lt;div th:if=&quot;${#lists.isEmpty(notifications)}&quot;&amp;gt;
            &amp;lt;p class=&quot;empty-message&quot;&amp;gt;새로운 알림이 없습니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:unless=&quot;${#lists.isEmpty(notifications)}&quot;&amp;gt;
            &amp;lt;div th:each=&quot;notification : ${notifications}&quot; class=&quot;notification-item&quot;&amp;gt;
                &amp;lt;div th:class=&quot;'notification ' + ${notification.type}&quot;&amp;gt;
                    &amp;lt;span th:text=&quot;${notification.message}&quot;&amp;gt;알림 메시지&amp;lt;/span&amp;gt;
                    &amp;lt;small th:text=&quot;${#temporals.format(notification.createdAt, 'MM-dd HH:mm')}&quot;&amp;gt;01-15 14:30&amp;lt;/small&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;반복 처리 성능 최적화&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;optimized-iteration&quot;&amp;gt;
    &amp;lt;!-- 페이징된 데이터 처리 --&amp;gt;
    &amp;lt;div class=&quot;paged-results&quot;&amp;gt;
        &amp;lt;div th:each=&quot;item : ${pagedItems}&quot; class=&quot;result-item&quot;&amp;gt;
            &amp;lt;h4 th:text=&quot;${item.title}&quot;&amp;gt;제목&amp;lt;/h4&amp;gt;
            &amp;lt;p th:text=&quot;${#strings.abbreviate(item.description, 100)}&quot;&amp;gt;설명...&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 페이징 정보 --&amp;gt;
        &amp;lt;div class=&quot;pagination-controls&quot;&amp;gt;
            &amp;lt;span&amp;gt;페이지 &amp;lt;span th:text=&quot;${currentPage + 1}&quot;&amp;gt;1&amp;lt;/span&amp;gt; / &amp;lt;span th:text=&quot;${totalPages}&quot;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;a th:if=&quot;${currentPage &amp;gt; 0}&quot; 
               th:href=&quot;@{''(page=${currentPage - 1})}&quot;&amp;gt;이전&amp;lt;/a&amp;gt;
            &amp;lt;a th:if=&quot;${currentPage &amp;lt; totalPages - 1}&quot; 
               th:href=&quot;@{''(page=${currentPage + 1})}&quot;&amp;gt;다음&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 부분 렌더링 (처음 N개만) --&amp;gt;
    &amp;lt;div class=&quot;limited-results&quot;&amp;gt;
        &amp;lt;h3&amp;gt;인기 상품 (상위 5개)&amp;lt;/h3&amp;gt;
        &amp;lt;div th:each=&quot;product, stat : ${popularProducts}&quot; 
             th:if=&quot;${stat.index &amp;lt; 5}&quot; 
             class=&quot;popular-product&quot;&amp;gt;
            &amp;lt;span class=&quot;rank&quot; th:text=&quot;${stat.count}&quot;&amp;gt;1&amp;lt;/span&amp;gt;
            &amp;lt;span th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/span&amp;gt;
            &amp;lt;span th:text=&quot;${product.viewCount}&quot;&amp;gt;조회수&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;a th:href=&quot;@{/products/popular}&quot;&amp;gt;전체 보기&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 조건부 상세 정보 로드 --&amp;gt;
    &amp;lt;div class=&quot;user-list-optimized&quot;&amp;gt;
        &amp;lt;div th:each=&quot;user, stat : ${users}&quot; class=&quot;user-summary&quot;&amp;gt;
            &amp;lt;div class=&quot;basic-info&quot;&amp;gt;
                &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
                &amp;lt;span th:text=&quot;${user.role}&quot;&amp;gt;역할&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 처음 10명만 상세 정보 표시 --&amp;gt;
            &amp;lt;div th:if=&quot;${stat.index &amp;lt; 10}&quot; class=&quot;detailed-info&quot;&amp;gt;
                &amp;lt;p&amp;gt;가입일: &amp;lt;span th:text=&quot;${#dates.format(user.joinDate, 'yyyy-MM-dd')}&quot;&amp;gt;2024-01-01&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;마지막 로그인: &amp;lt;span th:text=&quot;${#temporals.format(user.lastLogin, 'MM-dd HH:mm')}&quot;&amp;gt;01-15 14:30&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 나머지는 &quot;더보기&quot; 버튼으로 처리 --&amp;gt;
            &amp;lt;div th:if=&quot;${stat.index &amp;gt;= 10}&quot; class=&quot;load-more-info&quot;&amp;gt;
                &amp;lt;button th:onclick=&quot;|loadUserDetails(${user.id})|&quot;&amp;gt;상세정보&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.5 폼 관련 속성 (&lt;code&gt;th:field&lt;/code&gt;, &lt;code&gt;th:object&lt;/code&gt;, &lt;code&gt;th:action&lt;/code&gt;)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:object&lt;/code&gt;와 &lt;code&gt;th:field&lt;/code&gt; - 폼 바인딩&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;form-binding-examples&quot;&amp;gt;
    &amp;lt;!-- 기본 폼 바인딩 --&amp;gt;
    &amp;lt;form th:action=&quot;@{/user/save}&quot; th:object=&quot;${userForm}&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;name&quot;&amp;gt;이름&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;name&quot; th:field=&quot;*{name}&quot; class=&quot;form-control&quot;&amp;gt;
            &amp;lt;span th:if=&quot;${#fields.hasErrors('name')}&quot; 
                  th:errors=&quot;*{name}&quot; class=&quot;error&quot;&amp;gt;이름 오류&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;email&quot;&amp;gt;이메일&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; th:field=&quot;*{email}&quot; class=&quot;form-control&quot;&amp;gt;
            &amp;lt;span th:if=&quot;${#fields.hasErrors('email')}&quot; 
                  th:errors=&quot;*{email}&quot; class=&quot;error&quot;&amp;gt;이메일 오류&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;age&quot;&amp;gt;나이&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;number&quot; id=&quot;age&quot; th:field=&quot;*{age}&quot; min=&quot;1&quot; max=&quot;120&quot; class=&quot;form-control&quot;&amp;gt;
            &amp;lt;span th:if=&quot;${#fields.hasErrors('age')}&quot; 
                  th:errors=&quot;*{age}&quot; class=&quot;error&quot;&amp;gt;나이 오류&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;bio&quot;&amp;gt;자기소개&amp;lt;/label&amp;gt;
            &amp;lt;textarea id=&quot;bio&quot; th:field=&quot;*{bio}&quot; rows=&quot;4&quot; class=&quot;form-control&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
            &amp;lt;small class=&quot;form-text text-muted&quot;&amp;gt;200자 이내로 작성해주세요.&amp;lt;/small&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;button type=&quot;submit&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;다양한 입력 유형 처리&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;input-types&quot;&amp;gt;
    &amp;lt;form th:action=&quot;@{/profile/update}&quot; th:object=&quot;${profileForm}&quot; method=&quot;post&quot;&amp;gt;

        &amp;lt;!-- 체크박스 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;div class=&quot;checkbox&quot;&amp;gt;
                &amp;lt;label&amp;gt;
                    &amp;lt;input type=&quot;checkbox&quot; th:field=&quot;*{newsletter}&quot;&amp;gt;
                    뉴스레터 수신 동의
                &amp;lt;/label&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 다중 체크박스 --&amp;gt;
            &amp;lt;fieldset&amp;gt;
                &amp;lt;legend&amp;gt;관심 분야&amp;lt;/legend&amp;gt;
                &amp;lt;div th:each=&quot;interest : ${availableInterests}&quot;&amp;gt;
                    &amp;lt;label&amp;gt;
                        &amp;lt;input type=&quot;checkbox&quot; th:field=&quot;*{interests}&quot; th:value=&quot;${interest.code}&quot;&amp;gt;
                        &amp;lt;span th:text=&quot;${interest.name}&quot;&amp;gt;관심분야&amp;lt;/span&amp;gt;
                    &amp;lt;/label&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/fieldset&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 라디오 버튼 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;fieldset&amp;gt;
                &amp;lt;legend&amp;gt;성별&amp;lt;/legend&amp;gt;
                &amp;lt;label&amp;gt;
                    &amp;lt;input type=&quot;radio&quot; th:field=&quot;*{gender}&quot; value=&quot;M&quot;&amp;gt;
                    남성
                &amp;lt;/label&amp;gt;
                &amp;lt;label&amp;gt;
                    &amp;lt;input type=&quot;radio&quot; th:field=&quot;*{gender}&quot; value=&quot;F&quot;&amp;gt;
                    여성
                &amp;lt;/label&amp;gt;
                &amp;lt;label&amp;gt;
                    &amp;lt;input type=&quot;radio&quot; th:field=&quot;*{gender}&quot; value=&quot;O&quot;&amp;gt;
                    기타
                &amp;lt;/label&amp;gt;
            &amp;lt;/fieldset&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 선택 박스 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;country&quot;&amp;gt;국가&amp;lt;/label&amp;gt;
            &amp;lt;select id=&quot;country&quot; th:field=&quot;*{country}&quot; class=&quot;form-control&quot;&amp;gt;
                &amp;lt;option value=&quot;&quot;&amp;gt;선택해주세요&amp;lt;/option&amp;gt;
                &amp;lt;option th:each=&quot;country : ${countries}&quot; 
                        th:value=&quot;${country.code}&quot; 
                        th:text=&quot;${country.name}&quot;&amp;gt;국가명&amp;lt;/option&amp;gt;
            &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 다중 선택 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;skills&quot;&amp;gt;보유 기술&amp;lt;/label&amp;gt;
            &amp;lt;select id=&quot;skills&quot; th:field=&quot;*{skills}&quot; multiple class=&quot;form-control&quot;&amp;gt;
                &amp;lt;option th:each=&quot;skill : ${availableSkills}&quot; 
                        th:value=&quot;${skill.id}&quot; 
                        th:text=&quot;${skill.name}&quot;&amp;gt;기술명&amp;lt;/option&amp;gt;
            &amp;lt;/select&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 숨겨진 필드 --&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; th:field=&quot;*{id}&quot;&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; th:field=&quot;*{version}&quot;&amp;gt;

        &amp;lt;!-- 파일 업로드 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label for=&quot;avatar&quot;&amp;gt;프로필 사진&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;file&quot; id=&quot;avatar&quot; name=&quot;avatarFile&quot; accept=&quot;image/*&quot; class=&quot;form-control&quot;&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;button type=&quot;submit&quot;&amp;gt;업데이트&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;동적 폼과 배열 처리&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;dynamic-forms&quot;&amp;gt;
    &amp;lt;!-- 배열 필드 처리 --&amp;gt;
    &amp;lt;form th:action=&quot;@{/order/save}&quot; th:object=&quot;${orderForm}&quot; method=&quot;post&quot;&amp;gt;
        &amp;lt;h3&amp;gt;주문 정보&amp;lt;/h3&amp;gt;

        &amp;lt;!-- 주문 항목 목록 --&amp;gt;
        &amp;lt;div class=&quot;order-items&quot;&amp;gt;
            &amp;lt;div th:each=&quot;item, stat : *{items}&quot; class=&quot;order-item&quot;&amp;gt;
                &amp;lt;h4&amp;gt;항목 &amp;lt;span th:text=&quot;${stat.count}&quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/h4&amp;gt;

                &amp;lt;!-- 배열 인덱스 사용 --&amp;gt;
                &amp;lt;input type=&quot;hidden&quot; th:field=&quot;*{items[__${stat.index}__].id}&quot;&amp;gt;

                &amp;lt;div class=&quot;form-row&quot;&amp;gt;
                    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
                        &amp;lt;label th:for=&quot;'product_' + ${stat.index}&quot;&amp;gt;상품&amp;lt;/label&amp;gt;
                        &amp;lt;select th:id=&quot;'product_' + ${stat.index}&quot; 
                                th:field=&quot;*{items[__${stat.index}__].productId}&quot; 
                                class=&quot;form-control&quot;&amp;gt;
                            &amp;lt;option value=&quot;&quot;&amp;gt;선택하세요&amp;lt;/option&amp;gt;
                            &amp;lt;option th:each=&quot;product : ${products}&quot; 
                                    th:value=&quot;${product.id}&quot; 
                                    th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/option&amp;gt;
                        &amp;lt;/select&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
                        &amp;lt;label th:for=&quot;'quantity_' + ${stat.index}&quot;&amp;gt;수량&amp;lt;/label&amp;gt;
                        &amp;lt;input type=&quot;number&quot; th:id=&quot;'quantity_' + ${stat.index}&quot;
                               th:field=&quot;*{items[__${stat.index}__].quantity}&quot; 
                               min=&quot;1&quot; class=&quot;form-control&quot;&amp;gt;
                    &amp;lt;/div&amp;gt;

                    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
                        &amp;lt;label th:for=&quot;'price_' + ${stat.index}&quot;&amp;gt;가격&amp;lt;/label&amp;gt;
                        &amp;lt;input type=&quot;number&quot; th:id=&quot;'price_' + ${stat.index}&quot;
                               th:field=&quot;*{items[__${stat.index}__].price}&quot; 
                               step=&quot;0.01&quot; class=&quot;form-control&quot;&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;button type=&quot;button&quot; th:onclick=&quot;|removeItem(${stat.index})|&quot;&amp;gt;
                    항목 삭제
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;button type=&quot;button&quot; onclick=&quot;addOrderItem()&quot;&amp;gt;항목 추가&amp;lt;/button&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;주문 완료&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;고급 폼 검증과 오류 처리&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;advanced-validation&quot;&amp;gt;
    &amp;lt;form th:action=&quot;@{/registration}&quot; th:object=&quot;${registrationForm}&quot; method=&quot;post&quot; novalidate&amp;gt;

        &amp;lt;!-- 전역 오류 메시지 --&amp;gt;
        &amp;lt;div th:if=&quot;${#fields.hasGlobalErrors()}&quot; class=&quot;global-errors&quot;&amp;gt;
            &amp;lt;div class=&quot;alert alert-danger&quot;&amp;gt;
                &amp;lt;ul&amp;gt;
                    &amp;lt;li th:each=&quot;error : ${#fields.globalErrors()}&quot; th:text=&quot;${error}&quot;&amp;gt;전역 오류&amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 사용자명 검증 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot; th:classappend=&quot;${#fields.hasErrors('username')} ? 'has-error' : ''&quot;&amp;gt;
            &amp;lt;label for=&quot;username&quot;&amp;gt;사용자명 *&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;username&quot; th:field=&quot;*{username}&quot; 
                   class=&quot;form-control&quot;
                   th:classappend=&quot;${#fields.hasErrors('username')} ? 'is-invalid' : ''&quot;&amp;gt;

            &amp;lt;!-- 필드별 오류 메시지 --&amp;gt;
            &amp;lt;div th:if=&quot;${#fields.hasErrors('username')}&quot; class=&quot;invalid-feedback&quot;&amp;gt;
                &amp;lt;div th:each=&quot;error : ${#fields.errors('username')}&quot; th:text=&quot;${error}&quot;&amp;gt;
                    사용자명 오류
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 도움말 텍스트 --&amp;gt;
            &amp;lt;small class=&quot;form-text text-muted&quot;&amp;gt;
                3-20자의 영문, 숫자, 밑줄만 사용 가능
            &amp;lt;/small&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 비밀번호 검증 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot; th:classappend=&quot;${#fields.hasErrors('password')} ? 'has-error' : ''&quot;&amp;gt;
            &amp;lt;label for=&quot;password&quot;&amp;gt;비밀번호 *&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;password&quot; th:field=&quot;*{password}&quot; 
                   class=&quot;form-control&quot;
                   th:classappend=&quot;${#fields.hasErrors('password')} ? 'is-invalid' : ''&quot;&amp;gt;

            &amp;lt;div th:if=&quot;${#fields.hasErrors('password')}&quot; class=&quot;invalid-feedback&quot;&amp;gt;
                &amp;lt;div th:each=&quot;error : ${#fields.errors('password')}&quot; th:text=&quot;${error}&quot;&amp;gt;
                    비밀번호 오류
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- 실시간 비밀번호 강도 표시 --&amp;gt;
            &amp;lt;div class=&quot;password-strength&quot;&amp;gt;
                &amp;lt;div class=&quot;strength-meter&quot;&amp;gt;
                    &amp;lt;div class=&quot;strength-bar&quot; id=&quot;strengthBar&quot;&amp;gt;&amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;small id=&quot;strengthText&quot;&amp;gt;비밀번호를 입력하세요&amp;lt;/small&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 비밀번호 확인 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot; th:classappend=&quot;${#fields.hasErrors('confirmPassword')} ? 'has-error' : ''&quot;&amp;gt;
            &amp;lt;label for=&quot;confirmPassword&quot;&amp;gt;비밀번호 확인 *&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;password&quot; id=&quot;confirmPassword&quot; th:field=&quot;*{confirmPassword}&quot; 
                   class=&quot;form-control&quot;
                   th:classappend=&quot;${#fields.hasErrors('confirmPassword')} ? 'is-invalid' : ''&quot;&amp;gt;

            &amp;lt;div th:if=&quot;${#fields.hasErrors('confirmPassword')}&quot; class=&quot;invalid-feedback&quot;&amp;gt;
                &amp;lt;div th:each=&quot;error : ${#fields.errors('confirmPassword')}&quot; th:text=&quot;${error}&quot;&amp;gt;
                    비밀번호 확인 오류
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 이메일 검증 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot; th:classappend=&quot;${#fields.hasErrors('email')} ? 'has-error' : ''&quot;&amp;gt;
            &amp;lt;label for=&quot;email&quot;&amp;gt;이메일 *&amp;lt;/label&amp;gt;
            &amp;lt;div class=&quot;input-group&quot;&amp;gt;
                &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; th:field=&quot;*{email}&quot; 
                       class=&quot;form-control&quot;
                       th:classappend=&quot;${#fields.hasErrors('email')} ? 'is-invalid' : ''&quot;&amp;gt;
                &amp;lt;div class=&quot;input-group-append&quot;&amp;gt;
                    &amp;lt;button type=&quot;button&quot; class=&quot;btn btn-outline-secondary&quot; onclick=&quot;checkEmailDuplicate()&quot;&amp;gt;
                        중복확인
                    &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;div th:if=&quot;${#fields.hasErrors('email')}&quot; class=&quot;invalid-feedback&quot;&amp;gt;
                &amp;lt;div th:each=&quot;error : ${#fields.errors('email')}&quot; th:text=&quot;${error}&quot;&amp;gt;
                    이메일 오류
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 약관 동의 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot; th:classappend=&quot;${#fields.hasErrors('agreeToTerms')} ? 'has-error' : ''&quot;&amp;gt;
            &amp;lt;div class=&quot;form-check&quot;&amp;gt;
                &amp;lt;input type=&quot;checkbox&quot; id=&quot;agreeToTerms&quot; th:field=&quot;*{agreeToTerms}&quot; 
                       class=&quot;form-check-input&quot;
                       th:classappend=&quot;${#fields.hasErrors('agreeToTerms')} ? 'is-invalid' : ''&quot;&amp;gt;
                &amp;lt;label for=&quot;agreeToTerms&quot; class=&quot;form-check-label&quot;&amp;gt;
                    &amp;lt;a href=&quot;/terms&quot; target=&quot;_blank&quot;&amp;gt;이용약관&amp;lt;/a&amp;gt;에 동의합니다 *
                &amp;lt;/label&amp;gt;

                &amp;lt;div th:if=&quot;${#fields.hasErrors('agreeToTerms')}&quot; class=&quot;invalid-feedback&quot;&amp;gt;
                    &amp;lt;div th:each=&quot;error : ${#fields.errors('agreeToTerms')}&quot; th:text=&quot;${error}&quot;&amp;gt;
                        약관 동의 오류
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- CSRF 토큰 --&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; th:name=&quot;${_csrf.parameterName}&quot; th:value=&quot;${_csrf.token}&quot;&amp;gt;

        &amp;lt;!-- 제출 버튼 --&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;button type=&quot;submit&quot; class=&quot;btn btn-primary btn-block&quot;&amp;gt;
                회원가입
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
// 비밀번호 강도 검사
document.getElementById('password').addEventListener('input', function(e) {
    const password = e.target.value;
    const strengthBar = document.getElementById('strengthBar');
    const strengthText = document.getElementById('strengthText');

    let strength = 0;
    let message = '';

    if (password.length &amp;gt;= 8) strength++;
    if (/[a-z]/.test(password)) strength++;
    if (/[A-Z]/.test(password)) strength++;
    if (/[0-9]/.test(password)) strength++;
    if (/[^A-Za-z0-9]/.test(password)) strength++;

    switch (strength) {
        case 0:
        case 1:
            strengthBar.style.width = '20%';
            strengthBar.style.backgroundColor = '#dc3545';
            message = '매우 약함';
            break;
        case 2:
            strengthBar.style.width = '40%';
            strengthBar.style.backgroundColor = '#fd7e14';
            message = '약함';
            break;
        case 3:
            strengthBar.style.width = '60%';
            strengthBar.style.backgroundColor = '#ffc107';
            message = '보통';
            break;
        case 4:
            strengthBar.style.width = '80%';
            strengthBar.style.backgroundColor = '#20c997';
            message = '강함';
            break;
        case 5:
            strengthBar.style.width = '100%';
            strengthBar.style.backgroundColor = '#28a745';
            message = '매우 강함';
            break;
    }

    strengthText.textContent = message;
});

// 이메일 중복 확인
function checkEmailDuplicate() {
    const email = document.getElementById('email').value;
    if (!email) {
        alert('이메일을 입력해주세요.');
        return;
    }

    // AJAX 요청으로 중복 확인
    fetch(`/api/check-email?email=${encodeURIComponent(email)}`)
        .then(response =&amp;gt; response.json())
        .then(data =&amp;gt; {
            if (data.duplicate) {
                alert('이미 사용 중인 이메일입니다.');
            } else {
                alert('사용 가능한 이메일입니다.');
            }
        })
        .catch(error =&amp;gt; {
            console.error('Error:', error);
            alert('이메일 확인 중 오류가 발생했습니다.');
        });
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 5장 &quot;표준 속성&quot;을 완성했습니다. 이 장에서는 Thymeleaf의 핵심 속성들인 텍스트 출력, 속성 설정, 조건부 렌더링, 반복 처리, 그리고 폼 처리에 대해 상세하고 실용적인 예제들을 통해 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 섹션에서 다룬 내용:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;텍스트 출력&lt;/b&gt;: &lt;code&gt;th:text&lt;/code&gt;와 &lt;code&gt;th:utext&lt;/code&gt;의 차이점과 보안 고려사항&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속성 설정&lt;/b&gt;: 동적 속성 설정과 조건부 속성 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건부 렌더링&lt;/b&gt;: 복잡한 조건 로직과 다중 분기 처리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반복 처리&lt;/b&gt;: 상태 변수 활용과 중첩 반복, 성능 최적화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폼 처리&lt;/b&gt;: 다양한 입력 유형과 검증, 동적 폼 구성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 표준 속성들을 잘 활용하면 대부분의 웹 애플리케이션 요구사항을 충족할 수 있습니다.&lt;/p&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/144</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-5-%ED%91%9C%EC%A4%80%EC%86%8D%EC%84%B1-2#entry144comment</comments>
      <pubDate>Mon, 25 Aug 2025 11:55:05 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #5. 반복처리</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-5-%EB%B0%98%EB%B3%B5%EC%B2%98%EB%A6%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x681o/btsP6bpuNMC/K3qQ0djwk238A3dmfUKOuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x681o/btsP6bpuNMC/K3qQ0djwk238A3dmfUKOuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x681o/btsP6bpuNMC/K3qQ0djwk238A3dmfUKOuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx681o%2FbtsP6bpuNMC%2FK3qQ0djwk238A3dmfUKOuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;5장. 표준 속성 (Standard Attributes)&lt;/b&gt;&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 텍스트와 HTML 출력 (&lt;code&gt;th:text&lt;/code&gt;, &lt;code&gt;th:utext&lt;/code&gt;)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:text&lt;/code&gt; - 안전한 텍스트 출력&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;th:text&lt;/code&gt;는 가장 기본적인 Thymeleaf 속성으로, 텍스트를 안전하게 출력합니다. HTML 특수 문자들이 자동으로 이스케이프됩니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;text-examples&quot;&amp;gt;
    &amp;lt;!-- 기본 텍스트 출력 --&amp;gt;
    &amp;lt;h1 th:text=&quot;${pageTitle}&quot;&amp;gt;기본 페이지 제목&amp;lt;/h1&amp;gt;
    &amp;lt;p th:text=&quot;${user.name}&quot;&amp;gt;사용자 이름&amp;lt;/p&amp;gt;

    &amp;lt;!-- HTML 특수문자 이스케이프 --&amp;gt;
    &amp;lt;p th:text=&quot;${userInput}&quot;&amp;gt;사용자 입력: &amp;amp;lt;script&amp;amp;gt;alert('XSS')&amp;amp;lt;/script&amp;amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- 숫자와 날짜 출력 --&amp;gt;
    &amp;lt;p&amp;gt;나이: &amp;lt;span th:text=&quot;${user.age}&quot;&amp;gt;25&amp;lt;/span&amp;gt;세&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;가입일: &amp;lt;span th:text=&quot;${#dates.format(user.joinDate, 'yyyy-MM-dd')}&quot;&amp;gt;2024-01-15&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- null 값 처리 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.nickname ?: '닉네임 없음'}&quot;&amp;gt;닉네임&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.bio}&quot;&amp;gt;자기소개가 없습니다.&amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건부 텍스트 --&amp;gt;
    &amp;lt;span th:text=&quot;${user.isOnline()} ? '온라인' : '오프라인'&quot;&amp;gt;상태&amp;lt;/span&amp;gt;
    &amp;lt;span th:text=&quot;${order.isPaid()} ? '결제완료' : '미결제'&quot;&amp;gt;결제상태&amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:utext&lt;/code&gt; - HTML 내용 출력&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;th:utext&lt;/code&gt;는 HTML을 해석하여 출력합니다. 신뢰할 수 있는 HTML 내용에만 사용해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;utext-examples&quot;&amp;gt;
    &amp;lt;!-- 에디터에서 작성된 HTML 내용 --&amp;gt;
    &amp;lt;div class=&quot;article-content&quot; th:utext=&quot;${article.htmlContent}&quot;&amp;gt;
        &amp;lt;p&amp;gt;기본 &amp;lt;strong&amp;gt;HTML&amp;lt;/strong&amp;gt; 내용입니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 마크다운을 HTML로 변환한 내용 --&amp;gt;
    &amp;lt;div class=&quot;comment-body&quot; th:utext=&quot;${comment.renderedContent}&quot;&amp;gt;
        &amp;lt;p&amp;gt;렌더링된 댓글 내용&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 이메일 템플릿 --&amp;gt;
    &amp;lt;div class=&quot;email-body&quot; th:utext=&quot;${emailTemplate.body}&quot;&amp;gt;
        &amp;lt;h2&amp;gt;안녕하세요!&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;이메일 &amp;lt;em&amp;gt;내용&amp;lt;/em&amp;gt;입니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 조건부 HTML 출력 --&amp;gt;
    &amp;lt;div th:utext=&quot;${user.isVip()} ? '&amp;lt;span class=&amp;amp;quot;vip-badge&amp;amp;quot;&amp;gt;VIP&amp;lt;/span&amp;gt;' : ''&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;!-- 국제화된 HTML 메시지 --&amp;gt;
    &amp;lt;div th:utext=&quot;#{message.welcome.html(${user.name})}&quot;&amp;gt;
        환영합니다, &amp;lt;strong&amp;gt;사용자&amp;lt;/strong&amp;gt;님!
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:text&lt;/code&gt;와 &lt;code&gt;th:utext&lt;/code&gt; 보안 고려사항&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;security-examples&quot;&amp;gt;
    &amp;lt;!-- 안전한 사용 - 사용자 입력은 th:text 사용 --&amp;gt;
    &amp;lt;div class=&quot;user-comment&quot;&amp;gt;
        &amp;lt;h4 th:text=&quot;${comment.author.name}&quot;&amp;gt;작성자&amp;lt;/h4&amp;gt;
        &amp;lt;p th:text=&quot;${comment.text}&quot;&amp;gt;댓글 내용&amp;lt;/p&amp;gt;
        &amp;lt;small th:text=&quot;${#dates.format(comment.createdAt, 'yyyy-MM-dd HH:mm')}&quot;&amp;gt;작성일&amp;lt;/small&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 위험한 사용 - 사용자 입력을 th:utext로 출력하면 XSS 위험 --&amp;gt;
    &amp;lt;!-- &amp;lt;div th:utext=&quot;${userInput}&quot;&amp;gt;&amp;lt;/div&amp;gt; --&amp;gt; &amp;lt;!-- 절대 하지 말 것! --&amp;gt;

    &amp;lt;!-- 안전한 HTML 출력 - 서버에서 검증된 내용만 --&amp;gt;
    &amp;lt;div class=&quot;admin-notice&quot; th:if=&quot;${adminNotice}&quot; th:utext=&quot;${adminNotice.content}&quot;&amp;gt;
        &amp;lt;p&amp;gt;관리자 공지사항입니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- HTML 태그 제거 후 출력 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.unescapeHtml(#strings.escapeJava(userBio))}&quot;&amp;gt;
        HTML 태그가 제거된 자기소개
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건부 HTML 이스케이프 --&amp;gt;
    &amp;lt;div th:with=&quot;isAdminContent=${currentUser.isAdmin()}&quot;&amp;gt;
        &amp;lt;div th:if=&quot;${isAdminContent}&quot; th:utext=&quot;${content}&quot;&amp;gt;관리자 전용 HTML&amp;lt;/div&amp;gt;
        &amp;lt;div th:unless=&quot;${isAdminContent}&quot; th:text=&quot;${content}&quot;&amp;gt;일반 텍스트&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 속성 설정 (&lt;code&gt;th:attr&lt;/code&gt;, &lt;code&gt;th:attrappend&lt;/code&gt;, &lt;code&gt;th:attrprepend&lt;/code&gt;)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:attr&lt;/code&gt; - 일반적인 속성 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;attr-examples&quot;&amp;gt;
    &amp;lt;!-- 단일 속성 설정 --&amp;gt;
    &amp;lt;img th:attr=&quot;src=@{/images/users/{id}.jpg(id=${user.id})}, alt=${user.name}&quot;&amp;gt;

    &amp;lt;!-- 여러 속성 동시 설정 --&amp;gt;
    &amp;lt;a th:attr=&quot;href=@{/user/{id}(id=${user.id})}, 
                title=${user.name} + ' 프로필', 
                class=${user.isOnline()} ? 'user-link online' : 'user-link offline'&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
    &amp;lt;/a&amp;gt;

    &amp;lt;!-- 조건부 속성 설정 --&amp;gt;
    &amp;lt;input type=&quot;text&quot; 
           th:attr=&quot;placeholder=${#strings.isEmpty(user.name)} ? '이름을 입력하세요' : ${user.name},
                   maxlength=${user.isPremium()} ? '200' : '100'&quot;&amp;gt;

    &amp;lt;!-- 데이터 속성 설정 --&amp;gt;
    &amp;lt;div th:attr=&quot;data-user-id=${user.id},
                  data-role=${user.role},
                  data-premium=${user.isPremium()},
                  data-last-seen=${#dates.format(user.lastSeen, 'yyyy-MM-dd')}&quot;&amp;gt;
        사용자 정보
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 동적 ID와 클래스 --&amp;gt;
    &amp;lt;div th:attr=&quot;id='user-' + ${user.id},
                  class='card ' + ${user.role} + ' ' + (${user.isActive()} ? 'active' : 'inactive')&quot;&amp;gt;
        동적 속성
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:attrappend&lt;/code&gt; - 속성 값 추가&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;attrappend-examples&quot;&amp;gt;
    &amp;lt;!-- 클래스 추가 --&amp;gt;
    &amp;lt;div class=&quot;card&quot; th:attrappend=&quot;class=${user.isVip()} ? ' vip-card' : ''&quot;&amp;gt;
        기본 카드에 VIP 클래스 추가
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;user-profile&quot; 
         th:attrappend=&quot;class=${user.isOnline()} ? ' online' : ' offline'&quot;&amp;gt;
        온라인 상태 클래스 추가
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 스타일 추가 --&amp;gt;
    &amp;lt;div style=&quot;padding: 10px;&quot; 
         th:attrappend=&quot;style=${user.customColor} ? '; background-color: ' + ${user.customColor} : ''&quot;&amp;gt;
        사용자 정의 색상 추가
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 여러 조건부 클래스 추가 --&amp;gt;
    &amp;lt;div class=&quot;post&quot;
         th:attrappend=&quot;class=${post.isPinned()} ? ' pinned' : '',
                       class=${post.isHot()} ? ' hot' : '',
                       class=${post.hasImage()} ? ' with-image' : ''&quot;&amp;gt;
        게시글 상태 클래스들 추가
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 데이터 속성 추가 --&amp;gt;
    &amp;lt;button class=&quot;action-btn&quot; data-action=&quot;like&quot;
            th:attrappend=&quot;data-count=' ' + ${post.likeCount},
                          data-user-liked=${currentUser.hasLiked(post)} ? ' user-liked' : ''&quot;&amp;gt;
        좋아요
    &amp;lt;/button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:attrprepend&lt;/code&gt; - 속성 값 앞에 추가&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;attrprepend-examples&quot;&amp;gt;
    &amp;lt;!-- 클래스 앞에 추가 --&amp;gt;
    &amp;lt;div class=&quot;content&quot; th:attrprepend=&quot;class=${user.role} + ' '&quot;&amp;gt;
        역할별 클래스를 앞에 추가
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 경로 앞에 추가 --&amp;gt;
    &amp;lt;img src=&quot;default.jpg&quot; 
         th:attrprepend=&quot;src=${user.avatarPath} ? ${user.avatarPath} + '/' : '/images/'&quot;&amp;gt;

    &amp;lt;!-- ID 접두사 추가 --&amp;gt;
    &amp;lt;div id=&quot;content&quot; th:attrprepend=&quot;id=${pageType} + '-'&quot;&amp;gt;
        페이지 타입별 ID 접두사
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 조건부 접두사 --&amp;gt;
    &amp;lt;span class=&quot;badge&quot; 
          th:attrprepend=&quot;class=${notification.isUrgent()} ? 'urgent-' : 'normal-'&quot;&amp;gt;
        알림 배지
    &amp;lt;/span&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;특정 속성 직접 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;specific-attr-examples&quot;&amp;gt;
    &amp;lt;!-- 자주 사용되는 속성들의 직접 설정 --&amp;gt;

    &amp;lt;!-- th:href --&amp;gt;
    &amp;lt;a th:href=&quot;@{/user/{id}(id=${user.id})}&quot;&amp;gt;프로필 보기&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/products(category=${category.id}, page=${currentPage})}&quot;&amp;gt;상품 목록&amp;lt;/a&amp;gt;

    &amp;lt;!-- th:src --&amp;gt;
    &amp;lt;img th:src=&quot;@{/images/products/{id}.jpg(id=${product.id})}&quot; 
         th:alt=&quot;${product.name}&quot;&amp;gt;

    &amp;lt;!-- th:value --&amp;gt;
    &amp;lt;input type=&quot;text&quot; th:value=&quot;${user.name}&quot; name=&quot;name&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; th:value=&quot;${csrfToken}&quot; name=&quot;_token&quot;&amp;gt;

    &amp;lt;!-- th:id와 th:class --&amp;gt;
    &amp;lt;div th:id=&quot;'section-' + ${section.id}&quot; 
         th:class=&quot;'content-section ' + ${section.type}&quot;&amp;gt;
        섹션 내용
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- th:title과 th:alt --&amp;gt;
    &amp;lt;img th:src=&quot;@{/images/help.png}&quot; 
         th:alt=&quot;#{help.icon.alt}&quot; 
         th:title=&quot;#{help.icon.title}&quot;&amp;gt;

    &amp;lt;!-- th:style --&amp;gt;
    &amp;lt;div th:style=&quot;'width: ' + ${progress} + '%; background-color: ' + ${progressColor}&quot;&amp;gt;
        진행률 바
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- th:disabled와 th:readonly --&amp;gt;
    &amp;lt;input type=&quot;text&quot; th:value=&quot;${user.email}&quot; 
           th:readonly=&quot;${not user.canEditEmail()}&quot;&amp;gt;
    &amp;lt;button th:disabled=&quot;${not user.canPerformAction()}&quot; 
            th:text=&quot;#{action.submit}&quot;&amp;gt;제출&amp;lt;/button&amp;gt;

    &amp;lt;!-- th:checked와 th:selected --&amp;gt;
    &amp;lt;input type=&quot;checkbox&quot; th:checked=&quot;${user.receiveNewsletter}&quot;&amp;gt;
    &amp;lt;select name=&quot;country&quot;&amp;gt;
        &amp;lt;option th:each=&quot;country : ${countries}&quot;
                th:value=&quot;${country.code}&quot;
                th:text=&quot;${country.name}&quot;
                th:selected=&quot;${country.code == user.country}&quot;&amp;gt;국가&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 조건부 렌더링 (&lt;code&gt;th:if&lt;/code&gt;, &lt;code&gt;th:unless&lt;/code&gt;, &lt;code&gt;th:switch&lt;/code&gt;)&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:if&lt;/code&gt;와 &lt;code&gt;th:unless&lt;/code&gt; - 기본 조건부 렌더링&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-rendering&quot;&amp;gt;
    &amp;lt;!-- 기본 조건부 표시 --&amp;gt;
    &amp;lt;div th:if=&quot;${user != null}&quot;&amp;gt;
        &amp;lt;h3&amp;gt;로그인 사용자 정보&amp;lt;/h3&amp;gt;
        &amp;lt;p th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${user != null}&quot;&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;a th:href=&quot;@{/login}&quot;&amp;gt;로그인하세요&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 불리언 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.isActive()}&quot;&amp;gt;
        &amp;lt;span class=&quot;status active&quot;&amp;gt;활성 계정&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${user.isActive()}&quot;&amp;gt;
        &amp;lt;span class=&quot;status inactive&quot;&amp;gt;비활성 계정&amp;lt;/span&amp;gt;
        &amp;lt;p&amp;gt;계정을 활성화하려면 이메일을 확인하세요.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 숫자 비교 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.age &amp;gt;= 18}&quot;&amp;gt;
        &amp;lt;p&amp;gt;성인 인증이 완료되었습니다.&amp;lt;/p&amp;gt;
        &amp;lt;button&amp;gt;성인 콘텐츠 보기&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:if=&quot;${user.score &amp;gt;= 100}&quot;&amp;gt;
        &amp;lt;div class=&quot;achievement&quot;&amp;gt;
            &amp;lt;h4&amp;gt;  100점 달성!&amp;lt;/h4&amp;gt;
            &amp;lt;p&amp;gt;축하합니다!&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.role == 'admin'}&quot;&amp;gt;
        &amp;lt;nav class=&quot;admin-nav&quot;&amp;gt;
            &amp;lt;a th:href=&quot;@{/admin}&quot;&amp;gt;관리자 패널&amp;lt;/a&amp;gt;
            &amp;lt;a th:href=&quot;@{/admin/users}&quot;&amp;gt;사용자 관리&amp;lt;/a&amp;gt;
        &amp;lt;/nav&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${#strings.isEmpty(user.bio)}&quot;&amp;gt;
        &amp;lt;div class=&quot;user-bio&quot;&amp;gt;
            &amp;lt;h4&amp;gt;자기소개&amp;lt;/h4&amp;gt;
            &amp;lt;p th:text=&quot;${user.bio}&quot;&amp;gt;자기소개&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;복합 조건과 논리 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;complex-conditions&quot;&amp;gt;
    &amp;lt;!-- AND 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.isActive() and user.isVerified()}&quot;&amp;gt;
        &amp;lt;div class=&quot;full-access&quot;&amp;gt;
            &amp;lt;p&amp;gt;모든 기능을 사용할 수 있습니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- OR 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.isPremium() or user.score &amp;gt;= 90}&quot;&amp;gt;
        &amp;lt;div class=&quot;premium-features&quot;&amp;gt;
            &amp;lt;h4&amp;gt;프리미엄 기능&amp;lt;/h4&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li&amp;gt;광고 제거&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;고급 통계&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;우선 지원&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- NOT 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${not user.hasCompletedProfile()}&quot;&amp;gt;
        &amp;lt;div class=&quot;profile-incomplete-warning&quot;&amp;gt;
            &amp;lt;p&amp;gt;프로필을 완성해주세요!&amp;lt;/p&amp;gt;
            &amp;lt;a th:href=&quot;@{/profile/edit}&quot;&amp;gt;프로필 편집&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 복잡한 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${(user.isActive() and user.isVerified()) or user.isAdmin()}&quot;&amp;gt;
        &amp;lt;button&amp;gt;고급 설정&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 컬렉션 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${not #lists.isEmpty(user.orders)}&quot;&amp;gt;
        &amp;lt;div class=&quot;order-history&quot;&amp;gt;
            &amp;lt;h4&amp;gt;주문 내역&amp;lt;/h4&amp;gt;
            &amp;lt;p&amp;gt;총 &amp;lt;span th:text=&quot;${#lists.size(user.orders)}&quot;&amp;gt;0&amp;lt;/span&amp;gt;개의 주문&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${#lists.isEmpty(notifications)}&quot;&amp;gt;
        &amp;lt;div class=&quot;notifications&quot;&amp;gt;
            &amp;lt;span class=&quot;badge&quot; th:text=&quot;${#lists.size(notifications)}&quot;&amp;gt;5&amp;lt;/span&amp;gt;
            새 알림
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;&lt;code&gt;th:switch&lt;/code&gt;와 &lt;code&gt;th:case&lt;/code&gt; - 다중 분기&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;switch-examples&quot;&amp;gt;
    &amp;lt;!-- 기본 switch 문 --&amp;gt;
    &amp;lt;div th:switch=&quot;${user.role}&quot;&amp;gt;
        &amp;lt;div th:case=&quot;'admin'&quot;&amp;gt;
            &amp;lt;h3&amp;gt;관리자&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;시스템 전체를 관리할 수 있습니다.&amp;lt;/p&amp;gt;
            &amp;lt;a th:href=&quot;@{/admin}&quot; class=&quot;btn btn-danger&quot;&amp;gt;관리자 패널&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;'moderator'&quot;&amp;gt;
            &amp;lt;h3&amp;gt;운영자&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;콘텐츠를 관리할 수 있습니다.&amp;lt;/p&amp;gt;
            &amp;lt;a th:href=&quot;@{/moderation}&quot; class=&quot;btn btn-warning&quot;&amp;gt;운영 도구&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;'premium'&quot;&amp;gt;
            &amp;lt;h3&amp;gt;프리미엄 사용자&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;모든 프리미엄 기능을 이용할 수 있습니다.&amp;lt;/p&amp;gt;
            &amp;lt;span class=&quot;badge badge-gold&quot;&amp;gt;PREMIUM&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;*&quot;&amp;gt;
            &amp;lt;h3&amp;gt;일반 사용자&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;기본 기능을 이용할 수 있습니다.&amp;lt;/p&amp;gt;
            &amp;lt;a th:href=&quot;@{/upgrade}&quot; class=&quot;btn btn-primary&quot;&amp;gt;업그레이드&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 숫자 기반 switch --&amp;gt;
    &amp;lt;div class=&quot;grade-display&quot; th:switch=&quot;${student.grade}&quot;&amp;gt;
        &amp;lt;div th:case=&quot;'A'&quot;&amp;gt;
            &amp;lt;span class=&quot;grade excellent&quot;&amp;gt;A학점&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;우수한 성적입니다!  &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:case=&quot;'B'&quot;&amp;gt;
            &amp;lt;span class=&quot;grade good&quot;&amp;gt;B학점&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;양호한 성적입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:case=&quot;'C'&quot;&amp;gt;
            &amp;lt;span class=&quot;grade average&quot;&amp;gt;C학점&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;보통 성적입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:case=&quot;*&quot;&amp;gt;
            &amp;lt;span class=&quot;grade poor&quot;&amp;gt;재수강 필요&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;더 노력이 필요합니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 주문 상태별 처리 --&amp;gt;
    &amp;lt;div class=&quot;order-status&quot; th:switch=&quot;${order.status}&quot;&amp;gt;
        &amp;lt;div th:case=&quot;'PENDING'&quot;&amp;gt;
            &amp;lt;span class=&quot;status pending&quot;&amp;gt;⏳ 처리 대기중&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;주문이 접수되었습니다.&amp;lt;/p&amp;gt;
            &amp;lt;button th:onclick=&quot;|cancelOrder(${order.id})|&quot;&amp;gt;취소&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;'PROCESSING'&quot;&amp;gt;
            &amp;lt;span class=&quot;status processing&quot;&amp;gt;⚙️ 처리중&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;주문을 준비하고 있습니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;'SHIPPED'&quot;&amp;gt;
            &amp;lt;span class=&quot;status shipped&quot;&amp;gt;  배송중&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;상품이 배송 중입니다.&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;운송장번호: &amp;lt;code th:text=&quot;${order.trackingNumber}&quot;&amp;gt;123456789&amp;lt;/code&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;'DELIVERED'&quot;&amp;gt;
            &amp;lt;span class=&quot;status delivered&quot;&amp;gt;✅ 배송완료&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;배송이 완료되었습니다.&amp;lt;/p&amp;gt;
            &amp;lt;button th:onclick=&quot;|reviewOrder(${order.id})|&quot;&amp;gt;리뷰 작성&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;'CANCELLED'&quot;&amp;gt;
            &amp;lt;span class=&quot;status cancelled&quot;&amp;gt;❌ 취소됨&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;주문이 취소되었습니다.&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;환불은 3-5일 소요됩니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div th:case=&quot;*&quot;&amp;gt;
            &amp;lt;span class=&quot;status unknown&quot;&amp;gt;❓ 상태 불명&amp;lt;/span&amp;gt;
            &amp;lt;p&amp;gt;주문 상태를 확인할 수 없습니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 계절별 메시지 --&amp;gt;
    &amp;lt;div th:switch=&quot;${#dates.month(#dates.createNow())}&quot;&amp;gt;
        &amp;lt;div th:case=&quot;12&quot; th:case=&quot;1&quot; th:case=&quot;2&quot;&amp;gt;
            &amp;lt;div class=&quot;season winter&quot;&amp;gt;
                ❄️ 겨울입니다. 따뜻하게 입으세요!
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:case=&quot;3&quot; th:case=&quot;4&quot; th:case=&quot;5&quot;&amp;gt;
            &amp;lt;div class=&quot;season spring&quot;&amp;gt;
                  봄입니다. 꽃구경 어떠세요?
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:case=&quot;6&quot; th:case=&quot;7&quot; th:case=&quot;8&quot;&amp;gt;
            &amp;lt;div class=&quot;season summer&quot;&amp;gt;
                ☀️ 여름입니다. 시원한 음료 어떠세요?
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div th:case=&quot;*&quot;&amp;gt;
            &amp;lt;div class=&quot;season autumn&quot;&amp;gt;
                  가을입니다. 단풍구경 어떠세요?
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;조건부 CSS 클래스와 스타일&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-styling&quot;&amp;gt;
    &amp;lt;!-- 조건부 클래스 적용 --&amp;gt;
    &amp;lt;div th:class=&quot;${user.isOnline()} ? 'user-card online' : 'user-card offline'&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
        &amp;lt;span th:if=&quot;${user.isOnline()}&quot; class=&quot;status-indicator&quot;&amp;gt; &amp;lt;/span&amp;gt;
        &amp;lt;span th:unless=&quot;${user.isOnline()}&quot; class=&quot;status-indicator&quot;&amp;gt;⚫&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 여러 조건부 클래스 --&amp;gt;
    &amp;lt;article th:class=&quot;'post ' + 
                      (${post.isPinned()} ? 'pinned ' : '') + 
                      (${post.isHot()} ? 'hot ' : '') + 
                      (${post.hasImages()} ? 'with-images ' : '') +
                      ${post.category}&quot;&amp;gt;
        &amp;lt;h3 th:text=&quot;${post.title}&quot;&amp;gt;게시글 제목&amp;lt;/h3&amp;gt;
    &amp;lt;/article&amp;gt;

    &amp;lt;!-- 조건부 스타일 --&amp;gt;
    &amp;lt;div class=&quot;progress-bar&quot;&amp;gt;
        &amp;lt;div th:style=&quot;'width: ' + ${task.progress} + '%; ' + 
                       'background-color: ' + (${task.progress &amp;gt;= 100} ? 'green' : 
                                              ${task.progress &amp;gt;= 70} ? 'orange' : 'red')&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 테마별 스타일 --&amp;gt;
    &amp;lt;div th:style=&quot;${user.theme == 'dark'} ? 'background: #333; color: white;' : 
                   (${user.theme == 'blue'} ? 'background: #e3f2fd; color: #1565c0;' : 
                    'background: white; color: black;')&quot;&amp;gt;
        사용자 정의 테마
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/143</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-5-%EB%B0%98%EB%B3%B5%EC%B2%98%EB%A6%AC#entry143comment</comments>
      <pubDate>Mon, 25 Aug 2025 11:51:40 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #4. 표준 표현식 심화</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-4-%ED%91%9C%EC%A4%80-%ED%91%9C%ED%98%84%EC%8B%9D-%EC%8B%AC%ED%99%94</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZqCd8/btsP4UhImJe/IiYVXLL9ECKVGnfK9HQNRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZqCd8/btsP4UhImJe/IiYVXLL9ECKVGnfK9HQNRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZqCd8/btsP4UhImJe/IiYVXLL9ECKVGnfK9HQNRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZqCd8%2FbtsP4UhImJe%2FIiYVXLL9ECKVGnfK9HQNRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4장. 표준 표현식 심화&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 조건 연산자와 삼항 연산자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf에서 조건부 로직을 처리하는 다양한 방법들을 살펴보겠습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 삼항 연산자&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-examples&quot;&amp;gt;
    &amp;lt;!-- 기본 삼항 연산자 (condition ? trueValue : falseValue) --&amp;gt;
    &amp;lt;p th:text=&quot;${user.age &amp;gt;= 18} ? '성인' : '미성년자'&quot;&amp;gt;연령 구분&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.isActive()} ? '활성' : '비활성'&quot;&amp;gt;계정 상태&amp;lt;/p&amp;gt;

    &amp;lt;!-- 숫자 조건 --&amp;gt;
    &amp;lt;span th:class=&quot;${product.stock &amp;gt; 0} ? 'in-stock' : 'out-of-stock'&quot;
          th:text=&quot;${product.stock &amp;gt; 0} ? '재고있음' : '품절'&quot;&amp;gt;재고 상태&amp;lt;/span&amp;gt;

    &amp;lt;!-- 문자열 조건 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.isEmpty(user.nickname)} ? user.name : user.nickname&quot;&amp;gt;표시명&amp;lt;/p&amp;gt;

    &amp;lt;!-- null 조건 --&amp;gt;
    &amp;lt;img th:src=&quot;${user.profileImage != null} ? ${user.profileImage} : '/images/default.png'&quot;
         th:alt=&quot;${user.name}&quot;&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;중첩된 삼항 연산자&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;nested-conditionals&quot;&amp;gt;
    &amp;lt;!-- 다중 조건 분기 --&amp;gt;
    &amp;lt;span th:class=&quot;${user.role == 'admin'} ? 'badge-admin' : 
                   (${user.role == 'moderator'} ? 'badge-moderator' : 'badge-user')&quot;
          th:text=&quot;${user.role}&quot;&amp;gt;역할&amp;lt;/span&amp;gt;

    &amp;lt;!-- 점수 기반 등급 --&amp;gt;
    &amp;lt;div class=&quot;grade-display&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${user.score &amp;gt;= 90} ? 'A' : 
                       (${user.score &amp;gt;= 80} ? 'B' : 
                       (${user.score &amp;gt;= 70} ? 'C' : 
                       (${user.score &amp;gt;= 60} ? 'D' : 'F')))&quot;&amp;gt;등급&amp;lt;/span&amp;gt;

        &amp;lt;span th:style=&quot;${user.score &amp;gt;= 90} ? 'color: gold;' : 
                        (${user.score &amp;gt;= 80} ? 'color: silver;' : 
                        (${user.score &amp;gt;= 70} ? 'color: bronze;' : 'color: gray;'))&quot;&amp;gt;
            ★
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 복잡한 상태 판단 --&amp;gt;
    &amp;lt;div class=&quot;user-status&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${user.isActive()} ? 
                       (${user.isVerified()} ? '정상 사용자' : '미인증 사용자') : 
                       (${user.isSuspended()} ? '정지된 사용자' : '비활성 사용자')&quot;&amp;gt;
            사용자 상태
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 날짜 기반 조건 --&amp;gt;
    &amp;lt;div class=&quot;membership-status&quot; th:with=&quot;daysSinceJoin=${#temporals.daysBetween(user.joinDate, #temporals.createNow())}&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${daysSinceJoin &amp;lt; 7} ? '신규 회원' : 
                       (${daysSinceJoin &amp;lt; 30} ? '새 회원' : 
                       (${daysSinceJoin &amp;lt; 365} ? '일반 회원' : '베테랑 회원'))&quot;&amp;gt;
            회원 등급
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;조건부 HTML 속성&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-attributes&quot;&amp;gt;
    &amp;lt;!-- 조건부 클래스 --&amp;gt;
    &amp;lt;div th:class=&quot;${user.isOnline()} ? 'user-card online' : 'user-card offline'&quot;&amp;gt;
        &amp;lt;h3 th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/h3&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 조건부 스타일 --&amp;gt;
    &amp;lt;div th:style=&quot;${user.isPremium()} ? 'border: 2px solid gold;' : 'border: 1px solid gray;'&quot;&amp;gt;
        프리미엄 사용자 카드
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 조건부 링크 --&amp;gt;
    &amp;lt;a th:href=&quot;${user.hasProfile()} ? @{/user/{id}/profile(id=${user.id})} : @{/user/{id}/setup(id=${user.id})}&quot;
       th:text=&quot;${user.hasProfile()} ? '프로필 보기' : '프로필 설정'&quot;&amp;gt;링크&amp;lt;/a&amp;gt;

    &amp;lt;!-- 조건부 이벤트 핸들러 --&amp;gt;
    &amp;lt;button th:onclick=&quot;${user.canEdit()} ? |editUser(${user.id})| : 'alert(\'권한이 없습니다\')'&quot;&amp;gt;
        편집
    &amp;lt;/button&amp;gt;

    &amp;lt;!-- 조건부 데이터 속성 --&amp;gt;
    &amp;lt;div th:attr=&quot;data-status=${user.isActive()} ? 'active' : 'inactive',
                  data-role=${user.role},
                  data-premium=${user.isPremium()} ? 'yes' : 'no'&quot;&amp;gt;
        데이터 속성 예제
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Elvis 연산자와 No-Operation 토큰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elvis 연산자(&lt;code&gt;?:&lt;/code&gt;)와 No-Operation 토큰(&lt;code&gt;_&lt;/code&gt;)은 더 간결한 조건 처리를 가능하게 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Elvis 연산자 기본 사용법&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;elvis-examples&quot;&amp;gt;
    &amp;lt;!-- 기본 Elvis 연산자 (leftExpression ?: rightExpression) --&amp;gt;
    &amp;lt;h3 th:text=&quot;${user.nickname ?: user.name}&quot;&amp;gt;표시명&amp;lt;/h3&amp;gt;
    &amp;lt;p th:text=&quot;${user.bio ?: '자기소개가 없습니다.'}&quot;&amp;gt;자기소개&amp;lt;/p&amp;gt;

    &amp;lt;!-- null이나 빈 문자열 처리 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.company ?: '회사 정보 없음'}&quot;&amp;gt;회사&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.website ?: '#'}&quot;&amp;gt;웹사이트&amp;lt;/p&amp;gt;

    &amp;lt;!-- 숫자 기본값 --&amp;gt;
    &amp;lt;p&amp;gt;점수: &amp;lt;span th:text=&quot;${user.score ?: 0}&quot;&amp;gt;0&amp;lt;/span&amp;gt;점&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;레벨: &amp;lt;span th:text=&quot;${user.level ?: 1}&quot;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- 컬렉션 기본값 --&amp;gt;
    &amp;lt;p&amp;gt;취미 개수: &amp;lt;span th:text=&quot;${#lists.size(user.hobbies ?: {})}&quot;&amp;gt;0&amp;lt;/span&amp;gt;개&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;체인된 Elvis 연산자&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;chained-elvis&quot;&amp;gt;
    &amp;lt;!-- 여러 값 중 첫 번째 null이 아닌 값 --&amp;gt;
    &amp;lt;h3 th:text=&quot;${user.displayName ?: user.nickname ?: user.firstName ?: 'Unknown'}&quot;&amp;gt;
        표시명
    &amp;lt;/h3&amp;gt;

    &amp;lt;!-- 연락처 우선순위 --&amp;gt;
    &amp;lt;p&amp;gt;연락처: &amp;lt;span th:text=&quot;${user.mobile ?: user.phone ?: user.email ?: '연락처 없음'}&quot;&amp;gt;연락처&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- 이미지 fallback --&amp;gt;
    &amp;lt;img th:src=&quot;${user.avatar ?: user.profilePicture ?: '/images/default-avatar.png'}&quot; 
         th:alt=&quot;${user.name}&quot;&amp;gt;

    &amp;lt;!-- 주소 정보 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.address?.fullAddress ?: user.address?.city ?: '주소 미입력'}&quot;&amp;gt;주소&amp;lt;/p&amp;gt;

    &amp;lt;!-- 소셜 링크 --&amp;gt;
    &amp;lt;a th:href=&quot;${user.website ?: user.blog ?: user.socialMedia ?: '#'}&quot;
       th:text=&quot;${user.website ?: user.blog ?: user.socialMedia ?: 'No Link'}&quot;&amp;gt;링크&amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Safe Navigation과 Elvis 연산자 조합&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;safe-navigation-elvis&quot;&amp;gt;
    &amp;lt;!-- 중첩 객체의 안전한 접근 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.profile?.company?.name ?: '소속 없음'}&quot;&amp;gt;회사명&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.settings?.privacy?.email ?: 'public'}&quot;&amp;gt;이메일 공개 설정&amp;lt;/p&amp;gt;

    &amp;lt;!-- 컬렉션의 안전한 접근 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.orders?[0]?.orderNumber ?: '주문 없음'}&quot;&amp;gt;최근 주문번호&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.projects?[0]?.title ?: '프로젝트 없음'}&quot;&amp;gt;최근 프로젝트&amp;lt;/p&amp;gt;

    &amp;lt;!-- 메소드 체이닝과 Elvis --&amp;gt;
    &amp;lt;p th:text=&quot;${user.getLatestPost()?.getTitle()?.toUpperCase() ?: 'NO POSTS'}&quot;&amp;gt;
        최신 게시글 (대문자)
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 날짜 포맷과 Elvis --&amp;gt;
    &amp;lt;p th:text=&quot;${user.lastLogin != null} ? ${#temporals.format(user.lastLogin, 'yyyy-MM-dd')} : '로그인 기록 없음'&quot;&amp;gt;
        마지막 로그인
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- Safe Navigation + Elvis + 포맷팅 --&amp;gt;
    &amp;lt;p th:text=&quot;${#temporals.format(user.profile?.lastUpdate, 'yyyy-MM-dd') ?: '업데이트 없음'}&quot;&amp;gt;
        프로필 마지막 업데이트
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;No-Operation 토큰 활용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;no-operation-examples&quot;&amp;gt;
    &amp;lt;!-- 조건부 텍스트 표시 (조건이 거짓이면 원래 텍스트 유지) --&amp;gt;
    &amp;lt;p th:text=&quot;${user.isVip()} ? 'VIP 회원' : _&quot;&amp;gt;일반 텍스트&amp;lt;/p&amp;gt;
    &amp;lt;h2 th:text=&quot;${pageTitle} ?: _&quot;&amp;gt;기본 페이지 제목&amp;lt;/h2&amp;gt;

    &amp;lt;!-- 조건부 속성 설정 --&amp;gt;
    &amp;lt;div th:class=&quot;${user.isPremium()} ? 'premium-badge' : _&quot;
         th:attr=&quot;data-premium=${user.isPremium()} ? 'true' : _&quot;&amp;gt;
        조건부 속성
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 조건부 링크 --&amp;gt;
    &amp;lt;a th:href=&quot;${user.hasPermission('ADMIN')} ? @{/admin} : _&quot;
       th:text=&quot;관리자 페이지&quot;&amp;gt;관리자 페이지&amp;lt;/a&amp;gt;

    &amp;lt;!-- 조건부 이벤트 --&amp;gt;
    &amp;lt;button th:onclick=&quot;${user.canDelete()} ? |deleteItem(${item.id})| : _&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;

    &amp;lt;!-- 조건부 스타일 --&amp;gt;
    &amp;lt;div th:style=&quot;${item.isHighlighted()} ? 'background-color: yellow;' : _&quot;&amp;gt;
        하이라이트 항목
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 복잡한 조건부 처리 --&amp;gt;
    &amp;lt;div class=&quot;notification&quot; 
         th:class=&quot;${notification.isUrgent()} ? 'notification urgent' : 'notification'&quot;
         th:attr=&quot;data-sound=${notification.hasSound()} ? 'enabled' : _,
                  data-popup=${notification.isPopup()} ? 'true' : _&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${notification.message}&quot;&amp;gt;알림 메시지&amp;lt;/span&amp;gt;
        &amp;lt;button th:onclick=&quot;${notification.isDismissible()} ? |dismiss(${notification.id})| : _&quot;&amp;gt;
            닫기
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 문자열 연산과 비교 연산&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;문자열 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;string-operations&quot;&amp;gt;
    &amp;lt;!-- 기본 문자열 연결 --&amp;gt;
    &amp;lt;p th:text=&quot;'Hello' + ' ' + 'World'&quot;&amp;gt;Hello World&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.firstName} + ' ' + ${user.lastName}&quot;&amp;gt;전체 이름&amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 템플릿 (권장 방법) --&amp;gt;
    &amp;lt;p th:text=&quot;|안녕하세요, ${user.name}님!|&quot;&amp;gt;인사말&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;|${user.name} (${user.email})|&quot;&amp;gt;사용자 정보&amp;lt;/p&amp;gt;

    &amp;lt;!-- 복잡한 문자열 구성 --&amp;gt;
    &amp;lt;p th:text=&quot;|주문 #${order.id}: ${order.status} - ${#numbers.formatCurrency(order.total)}|&quot;&amp;gt;
        주문 정보
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건부 문자열 구성 --&amp;gt;
    &amp;lt;p th:text=&quot;|상태: ${user.isActive() ? '활성' : '비활성'} | 레벨: ${user.level ?: 1}|&quot;&amp;gt;
        사용자 상태 정보
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열과 숫자 혼합 --&amp;gt;
    &amp;lt;p th:text=&quot;|총 ${#lists.size(products)}개 상품 중 ${#lists.size(products.?[inStock])}개 재고 있음|&quot;&amp;gt;
        재고 현황
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- URL과 문자열 조합 --&amp;gt;
    &amp;lt;a th:href=&quot;|/user/${user.id}/posts?page=${currentPage}|&quot; 
       th:text=&quot;|${user.name}의 게시글 보기|&quot;&amp;gt;게시글 링크&amp;lt;/a&amp;gt;

    &amp;lt;!-- CSS 클래스명 동적 생성 --&amp;gt;
    &amp;lt;div th:class=&quot;|user-card ${user.role} ${user.isOnline() ? 'online' : 'offline'}|&quot;&amp;gt;
        동적 클래스
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 이미지 경로 생성 --&amp;gt;
    &amp;lt;img th:src=&quot;|/images/avatars/${user.id}_${user.avatarVersion ?: 'default'}.jpg|&quot; 
         th:alt=&quot;|${user.name} 아바타|&quot;&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;문자열 비교 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;string-comparison&quot;&amp;gt;
    &amp;lt;!-- 기본 문자열 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${user.role == 'admin'}&quot;&amp;gt;관리자입니다.&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${user.status != 'banned'}&quot;&amp;gt;정상 사용자입니다.&amp;lt;/p&amp;gt;

    &amp;lt;!-- 대소문자 무시 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${#strings.equalsIgnoreCase(user.role, 'ADMIN')}&quot;&amp;gt;
        관리자 권한 (대소문자 무시)
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 포함 검사 --&amp;gt;
    &amp;lt;div th:if=&quot;${#strings.contains(user.email, '@gmail.com')}&quot;&amp;gt;
        Gmail 사용자입니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 시작/끝 검사 --&amp;gt;
    &amp;lt;div th:if=&quot;${#strings.startsWith(user.phone, '010')}&quot;&amp;gt;
        휴대폰 번호입니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:if=&quot;${#strings.endsWith(user.website, '.com')}&quot;&amp;gt;
        .com 도메인입니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 빈 문자열 검사 --&amp;gt;
    &amp;lt;div th:if=&quot;${#strings.isEmpty(user.bio)}&quot;&amp;gt;
        자기소개가 비어있습니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${#strings.isEmpty(user.nickname)}&quot;&amp;gt;
        닉네임: &amp;lt;span th:text=&quot;${user.nickname}&quot;&amp;gt;닉네임&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 길이 비교 --&amp;gt;
    &amp;lt;div th:if=&quot;${#strings.length(user.password) &amp;lt; 8}&quot;&amp;gt;
        비밀번호가 너무 짧습니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 정규식 매칭 --&amp;gt;
    &amp;lt;div class=&quot;validation-results&quot;&amp;gt;
        &amp;lt;p th:if=&quot;${#strings.matches(user.email, '^[A-Za-z0-9+_.-]+@(.+))}&quot;&amp;gt;
            유효한 이메일 형식입니다.
        &amp;lt;/p&amp;gt;

        &amp;lt;p th:if=&quot;${#strings.matches(user.phone, '^01[016789]-?[0-9]{3,4}-?[0-9]{4})}&quot;&amp;gt;
            유효한 휴대폰 번호입니다.
        &amp;lt;/p&amp;gt;

        &amp;lt;p th:if=&quot;${#strings.matches(user.zipCode, '^[0-9]{5})}&quot;&amp;gt;
            유효한 우편번호입니다.
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 복합 문자열 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${#strings.contains(user.email, 'admin') and #strings.endsWith(user.email, '.com')}&quot;&amp;gt;
        관리자 이메일로 추정됩니다.
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;고급 문자열 처리&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;advanced-string-operations&quot;&amp;gt;
    &amp;lt;!-- 문자열 변환 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.toUpperCase(user.name)}&quot;&amp;gt;이름 (대문자)&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.toLowerCase(user.email)}&quot;&amp;gt;이메일 (소문자)&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.capitalizeWords(user.address)}&quot;&amp;gt;주소 (단어별 대문자)&amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 자르기 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.substring(user.bio, 0, 100)} + ${#strings.length(user.bio) &amp;gt; 100 ? '...' : ''}&quot;&amp;gt;
        자기소개 미리보기
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 분할과 조인 --&amp;gt;
    &amp;lt;div th:with=&quot;tags=${#strings.listSplit(user.tags, ',')}&quot;&amp;gt;
        &amp;lt;span th:each=&quot;tag, stat : ${tags}&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${#strings.trim(tag)}&quot;&amp;gt;태그&amp;lt;/span&amp;gt;
            &amp;lt;span th:if=&quot;${!stat.last}&quot;&amp;gt;, &amp;lt;/span&amp;gt;
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 치환 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.replace(user.bio, '\n', '&amp;lt;br&amp;gt;')}&quot;&amp;gt;
        줄바꿈 처리된 자기소개
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 패딩 --&amp;gt;
    &amp;lt;div class=&quot;user-id&quot;&amp;gt;
        ID: &amp;lt;span th:text=&quot;${#strings.leftPad(#strings.toString(user.id), 6, '0')}&quot;&amp;gt;000001&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 트림 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.trim(user.company)}&quot;&amp;gt;회사명 (공백 제거)&amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 null 안전 처리 --&amp;gt;
    &amp;lt;p th:text=&quot;${#strings.defaultString(user.nickname, '닉네임 없음')}&quot;&amp;gt;닉네임&amp;lt;/p&amp;gt;

    &amp;lt;!-- 복잡한 문자열 가공 --&amp;gt;
    &amp;lt;div class=&quot;formatted-address&quot; th:if=&quot;${user.address}&quot;&amp;gt;
        &amp;lt;span th:text=&quot;${#strings.abbreviate(#strings.capitalizeWords(user.address.street), 20)}&quot;&amp;gt;
            주소
        &amp;lt;/span&amp;gt;
        &amp;lt;br&amp;gt;
        &amp;lt;span th:text=&quot;${#strings.toUpperCase(user.address.city)}&quot;&amp;gt;도시&amp;lt;/span&amp;gt;
        &amp;lt;span th:text=&quot;${user.address.zipCode}&quot;&amp;gt;우편번호&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 산술 연산과 논리 연산&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;산술 연산 심화&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;advanced-arithmetic&quot;&amp;gt;
    &amp;lt;!-- 기본 산술 연산 --&amp;gt;
    &amp;lt;div class=&quot;price-calculation&quot;&amp;gt;
        &amp;lt;p&amp;gt;상품 가격: &amp;lt;span th:text=&quot;${product.basePrice}&quot;&amp;gt;10000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;할인율: &amp;lt;span th:text=&quot;${product.discountRate}&quot;&amp;gt;10&amp;lt;/span&amp;gt;%&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;할인 금액: &amp;lt;span th:text=&quot;${product.basePrice * product.discountRate / 100}&quot;&amp;gt;1000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;최종 가격: &amp;lt;span th:text=&quot;${product.basePrice * (100 - product.discountRate) / 100}&quot;&amp;gt;9000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 복잡한 계산 --&amp;gt;
    &amp;lt;div class=&quot;shopping-cart&quot; th:with=&quot;total=${#aggregates.sum(cartItems.![quantity * price])}&quot;&amp;gt;
        &amp;lt;div th:each=&quot;item : ${cartItems}&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${item.name}&quot;&amp;gt;상품명&amp;lt;/span&amp;gt;:
            &amp;lt;span th:text=&quot;${item.quantity}&quot;&amp;gt;2&amp;lt;/span&amp;gt; &amp;times; 
            &amp;lt;span th:text=&quot;${#numbers.formatCurrency(item.price)}&quot;&amp;gt;₩10,000&amp;lt;/span&amp;gt; = 
            &amp;lt;span th:text=&quot;${#numbers.formatCurrency(item.quantity * item.price)}&quot;&amp;gt;₩20,000&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;cart-summary&quot;&amp;gt;
            &amp;lt;p&amp;gt;소계: &amp;lt;span th:text=&quot;${#numbers.formatCurrency(total)}&quot;&amp;gt;₩50,000&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;배송비: &amp;lt;span th:text=&quot;${total &amp;gt;= 30000} ? '무료' : '₩3,000'&quot;&amp;gt;무료&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
            &amp;lt;p&amp;gt;총합계: 
                &amp;lt;span th:text=&quot;${#numbers.formatCurrency(total + (total &amp;gt;= 30000 ? 0 : 3000))}&quot;&amp;gt;
                    ₩50,000
                &amp;lt;/span&amp;gt;
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 수학 함수 활용 --&amp;gt;
    &amp;lt;div class=&quot;math-functions&quot;&amp;gt;
        &amp;lt;!-- 반올림 --&amp;gt;
        &amp;lt;p&amp;gt;평균 점수: &amp;lt;span th:text=&quot;${#numbers.formatDecimal(totalScore / studentCount, 1, 2)}&quot;&amp;gt;85.67&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

        &amp;lt;!-- 최대/최소값 --&amp;gt;
        &amp;lt;p&amp;gt;최고 점수: &amp;lt;span th:text=&quot;${#aggregates.max(scores)}&quot;&amp;gt;95&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;최저 점수: &amp;lt;span th:text=&quot;${#aggregates.min(scores)}&quot;&amp;gt;72&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

        &amp;lt;!-- 절댓값 --&amp;gt;
        &amp;lt;p&amp;gt;차이: &amp;lt;span th:text=&quot;${#numbers.formatInteger(#math.abs(score1 - score2), 0, 'COMMA')}&quot;&amp;gt;15&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

        &amp;lt;!-- 거듭제곱과 제곱근 --&amp;gt;
        &amp;lt;p&amp;gt;제곱: &amp;lt;span th:text=&quot;${value * value}&quot;&amp;gt;25&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;세제곱: &amp;lt;span th:text=&quot;${value * value * value}&quot;&amp;gt;125&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 날짜 계산 --&amp;gt;
    &amp;lt;div class=&quot;date-calculations&quot; th:with=&quot;now=${#dates.createNow()}&quot;&amp;gt;
        &amp;lt;p&amp;gt;가입한지: 
            &amp;lt;span th:text=&quot;${#dates.daysBetween(user.joinDate, now)}&quot;&amp;gt;30&amp;lt;/span&amp;gt;일 경과
        &amp;lt;/p&amp;gt;

        &amp;lt;p&amp;gt;다음 결제일까지: 
            &amp;lt;span th:text=&quot;${#dates.daysBetween(now, user.nextPaymentDate)}&quot;&amp;gt;15&amp;lt;/span&amp;gt;일 남음
        &amp;lt;/p&amp;gt;

        &amp;lt;!-- 나이 계산 --&amp;gt;
        &amp;lt;p&amp;gt;나이: 
            &amp;lt;span th:text=&quot;${#dates.year(now) - #dates.year(user.birthDate)}&quot;&amp;gt;25&amp;lt;/span&amp;gt;세
        &amp;lt;/p&amp;gt;

        &amp;lt;!-- 근무 연차 계산 --&amp;gt;
        &amp;lt;p&amp;gt;근무 연차: 
            &amp;lt;span th:text=&quot;${(#dates.daysBetween(user.hireDate, now) / 365.0) + 1}&quot;&amp;gt;3.2&amp;lt;/span&amp;gt;년
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;논리 연산 심화&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;advanced-logical-operations&quot;&amp;gt;
    &amp;lt;!-- 복합 논리 조건 --&amp;gt;
    &amp;lt;div class=&quot;access-control&quot;&amp;gt;
        &amp;lt;!-- 관리자이거나 자신의 프로필인 경우 --&amp;gt;
        &amp;lt;div th:if=&quot;${currentUser.isAdmin() or currentUser.id == user.id}&quot;&amp;gt;
            &amp;lt;button&amp;gt;편집&amp;lt;/button&amp;gt;
            &amp;lt;button&amp;gt;삭제&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 활성 상태이고 인증된 사용자만 --&amp;gt;
        &amp;lt;div th:if=&quot;${user.isActive() and user.isVerified() and not user.isSuspended()}&quot;&amp;gt;
            &amp;lt;p&amp;gt;모든 기능을 사용할 수 있습니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 프리미엄이거나 높은 점수를 가진 사용자 --&amp;gt;
        &amp;lt;div th:if=&quot;${user.isPremium() or user.score &amp;gt;= 90}&quot;&amp;gt;
            &amp;lt;div class=&quot;premium-features&quot;&amp;gt;
                &amp;lt;p&amp;gt;프리미엄 기능 이용 가능&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 날짜 기반 논리 연산 --&amp;gt;
    &amp;lt;div class=&quot;date-based-logic&quot; th:with=&quot;now=${#temporals.createNow()}&quot;&amp;gt;
        &amp;lt;!-- 신규 사용자 (가입 후 7일 이내) --&amp;gt;
        &amp;lt;div th:if=&quot;${#temporals.daysBetween(user.joinDate, now) &amp;lt;= 7}&quot;&amp;gt;
            &amp;lt;div class=&quot;welcome-message&quot;&amp;gt;
                &amp;lt;p&amp;gt;신규 회원 환영합니다!  &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 오랫동안 활동하지 않은 사용자 --&amp;gt;
        &amp;lt;div th:if=&quot;${user.lastLoginDate != null and #temporals.daysBetween(user.lastLoginDate, now) &amp;gt; 30}&quot;&amp;gt;
            &amp;lt;div class=&quot;inactive-warning&quot;&amp;gt;
                &amp;lt;p&amp;gt;오랫동안 로그인하지 않았습니다. 계정을 확인해주세요.&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 구독 만료 임박 --&amp;gt;
        &amp;lt;div th:if=&quot;${user.subscriptionEndDate != null and #temporals.daysBetween(now, user.subscriptionEndDate) &amp;lt;= 7 and #temporals.daysBetween(now, user.subscriptionEndDate) &amp;gt;= 0}&quot;&amp;gt;
            &amp;lt;div class=&quot;subscription-warning&quot;&amp;gt;
                &amp;lt;p&amp;gt;구독이 곧 만료됩니다. 갱신해주세요.&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 컬렉션 기반 논리 연산 --&amp;gt;
    &amp;lt;div class=&quot;collection-based-logic&quot;&amp;gt;
        &amp;lt;!-- 빈 컬렉션 검사 --&amp;gt;
        &amp;lt;div th:if=&quot;${#lists.isEmpty(user.orders)}&quot;&amp;gt;
            &amp;lt;p&amp;gt;아직 주문 내역이 없습니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 컬렉션 크기 조건 --&amp;gt;
        &amp;lt;div th:if=&quot;${#lists.size(user.friends) &amp;gt;= 10}&quot;&amp;gt;
            &amp;lt;div class=&quot;social-badge&quot;&amp;gt;
                &amp;lt;p&amp;gt;인기 사용자 배지  &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 컬렉션 내용 검사 --&amp;gt;
        &amp;lt;div th:if=&quot;${#lists.contains(user.roles, 'EDITOR')}&quot;&amp;gt;
            &amp;lt;button&amp;gt;글쓰기&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 모든/일부 조건 --&amp;gt;
        &amp;lt;div th:if=&quot;${not #lists.isEmpty(user.projects) and #lists.size(user.projects.?[completed]) == #lists.size(user.projects)}&quot;&amp;gt;
            &amp;lt;div class=&quot;completion-badge&quot;&amp;gt;
                &amp;lt;p&amp;gt;모든 프로젝트 완료!  &amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 문자열 기반 논리 연산 --&amp;gt;
    &amp;lt;div class=&quot;string-based-logic&quot;&amp;gt;
        &amp;lt;!-- 이메일 도메인 검사 --&amp;gt;
        &amp;lt;div th:if=&quot;${#strings.contains(user.email, '@company.com') or #strings.contains(user.email, '@partner.com')}&quot;&amp;gt;
            &amp;lt;div class=&quot;corporate-user&quot;&amp;gt;
                &amp;lt;p&amp;gt;기업 사용자입니다.&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 전화번호 유효성과 지역 검사 --&amp;gt;
        &amp;lt;div th:if=&quot;${not #strings.isEmpty(user.phone) and (#strings.startsWith(user.phone, '02') or #strings.startsWith(user.phone, '010'))}&quot;&amp;gt;
            &amp;lt;p&amp;gt;서울 또는 휴대폰 번호입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 사용자명 길이와 형식 검사 --&amp;gt;
        &amp;lt;div th:if=&quot;${#strings.length(user.username) &amp;gt;= 3 and #strings.length(user.username) &amp;lt;= 20 and #strings.matches(user.username, '^[a-zA-Z0-9_]+)}&quot;&amp;gt;
            &amp;lt;p&amp;gt;유효한 사용자명입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.5 정규 표현식 활용&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 정규식 매칭&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;regex-examples&quot;&amp;gt;
    &amp;lt;!-- 이메일 유효성 검사 --&amp;gt;
    &amp;lt;div class=&quot;email-validation&quot;&amp;gt;
        &amp;lt;p th:if=&quot;${#strings.matches(user.email, '^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}))}&quot;&amp;gt;
            ✅ 유효한 이메일 주소
        &amp;lt;/p&amp;gt;
        &amp;lt;p th:unless=&quot;${#strings.matches(user.email, '^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\.[A-Za-z]{2,}))}&quot;&amp;gt;
            ❌ 이메일 형식이 올바르지 않습니다
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 전화번호 유효성 검사 --&amp;gt;
    &amp;lt;div class=&quot;phone-validation&quot;&amp;gt;
        &amp;lt;div th:switch=&quot;${true}&quot;&amp;gt;
            &amp;lt;p th:case=&quot;${#strings.matches(user.phone, '^01[016789]-?[0-9]{3,4}-?[0-9]{4})}&quot;&amp;gt;
                  유효한 휴대폰 번호
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:case=&quot;${#strings.matches(user.phone, '^0[2-9][0-9]?-?[0-9]{3,4}-?[0-9]{4})}&quot;&amp;gt;
                  유효한 일반 전화번호
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:case=&quot;*&quot;&amp;gt;
                ❌ 전화번호 형식이 올바르지 않습니다
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 비밀번호 강도 검사 --&amp;gt;
    &amp;lt;div class=&quot;password-strength&quot; th:with=&quot;password=${userForm.password}&quot;&amp;gt;
        &amp;lt;div class=&quot;strength-indicators&quot;&amp;gt;
            &amp;lt;span th:class=&quot;${#strings.matches(password, '.*[a-z].*')} ? 'valid' : 'invalid'&quot;&amp;gt;소문자&amp;lt;/span&amp;gt;
            &amp;lt;span th:class=&quot;${#strings.matches(password, '.*[A-Z].*')} ? 'valid' : 'invalid'&quot;&amp;gt;대문자&amp;lt;/span&amp;gt;
            &amp;lt;span th:class=&quot;${#strings.matches(password, '.*[0-9].*')} ? 'valid' : 'invalid'&quot;&amp;gt;숫자&amp;lt;/span&amp;gt;
            &amp;lt;span th:class=&quot;${#strings.matches(password, '.*[!@#$%^&amp;amp;*(),.?\&quot;:{}|&amp;lt;&amp;gt;].*')} ? 'valid' : 'invalid'&quot;&amp;gt;특수문자&amp;lt;/span&amp;gt;
            &amp;lt;span th:class=&quot;${#strings.length(password) &amp;gt;= 8} ? 'valid' : 'invalid'&quot;&amp;gt;8자 이상&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 전체 강도 평가 --&amp;gt;
        &amp;lt;div th:with=&quot;strongPassword=${#strings.matches(password, '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&amp;amp;*(),.?\&quot;:{}|&amp;lt;&amp;gt;]).{8,})}&quot;&amp;gt;
            &amp;lt;p th:if=&quot;${strongPassword}&quot; class=&quot;strong&quot;&amp;gt;  강력한 비밀번호&amp;lt;/p&amp;gt;
            &amp;lt;p th:unless=&quot;${strongPassword}&quot; class=&quot;weak&quot;&amp;gt;⚠️ 비밀번호를 더 강력하게 만드세요&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- URL 유효성 검사 --&amp;gt;
    &amp;lt;div class=&quot;url-validation&quot;&amp;gt;
        &amp;lt;a th:if=&quot;${#strings.matches(user.website, '^https?://.*)}&quot; 
           th:href=&quot;${user.website}&quot; 
           th:text=&quot;${user.website}&quot;&amp;gt;웹사이트&amp;lt;/a&amp;gt;

        &amp;lt;span th:unless=&quot;${#strings.matches(user.website, '^https?://.*)}&quot;
              th:text=&quot;${user.website}&quot;&amp;gt;유효하지 않은 URL&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;입력 값 검증과 형식화&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;input-validation&quot;&amp;gt;
    &amp;lt;!-- 사용자명 검증 --&amp;gt;
    &amp;lt;div class=&quot;username-validation&quot; th:with=&quot;username=${userForm.username}&quot;&amp;gt;
        &amp;lt;div class=&quot;validation-rules&quot;&amp;gt;
            &amp;lt;p th:class=&quot;${#strings.matches(username, '^[a-zA-Z0-9_]{3,20})} ? 'valid' : 'invalid'&quot;&amp;gt;
                영문, 숫자, 밑줄만 사용, 3-20자
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:class=&quot;${#strings.matches(username, '^[a-zA-Z].*')} ? 'valid' : 'invalid'&quot;&amp;gt;
                영문으로 시작
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:class=&quot;${not #strings.matches(username, '.*[_]{2,}.*')} ? 'valid' : 'invalid'&quot;&amp;gt;
                연속된 밑줄 사용 금지
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 주민등록번호 검증 (예시) --&amp;gt;
    &amp;lt;div class=&quot;ssn-validation&quot; th:if=&quot;${userForm.ssn}&quot;&amp;gt;
        &amp;lt;p th:if=&quot;${#strings.matches(userForm.ssn, '^[0-9]{6}-[1-4][0-9]{6})}&quot; class=&quot;valid&quot;&amp;gt;
            ✅ 올바른 주민등록번호 형식
        &amp;lt;/p&amp;gt;
        &amp;lt;p th:unless=&quot;${#strings.matches(userForm.ssn, '^[0-9]{6}-[1-4][0-9]{6})}&quot; class=&quot;invalid&quot;&amp;gt;
            ❌ 주민등록번호 형식이 올바르지 않습니다
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 신용카드 번호 검증 --&amp;gt;
    &amp;lt;div class=&quot;card-validation&quot; th:if=&quot;${paymentForm.cardNumber}&quot;&amp;gt;
        &amp;lt;div th:switch=&quot;${true}&quot;&amp;gt;
            &amp;lt;p th:case=&quot;${#strings.matches(paymentForm.cardNumber, '^4[0-9]{12}(?:[0-9]{3})?)}&quot;&amp;gt;
                  Visa 카드
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:case=&quot;${#strings.matches(paymentForm.cardNumber, '^5[1-5][0-9]{14})}&quot;&amp;gt;
                  MasterCard
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:case=&quot;${#strings.matches(paymentForm.cardNumber, '^3[47][0-9]{13})}&quot;&amp;gt;
                  American Express
            &amp;lt;/p&amp;gt;
            &amp;lt;p th:case=&quot;*&quot; class=&quot;invalid&quot;&amp;gt;
                ❌ 지원하지 않는 카드입니다
            &amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- IP 주소 검증 --&amp;gt;
    &amp;lt;div class=&quot;ip-validation&quot; th:if=&quot;${serverConfig.ipAddress}&quot;&amp;gt;
        &amp;lt;p th:if=&quot;${#strings.matches(serverConfig.ipAddress, '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))}&quot; class=&quot;valid&quot;&amp;gt;
              유효한 IPv4 주소
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;텍스트 추출과 변환&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;text-extraction&quot;&amp;gt;
    &amp;lt;!-- 해시태그 추출 --&amp;gt;
    &amp;lt;div class=&quot;hashtag-extraction&quot; th:if=&quot;${post.content}&quot;&amp;gt;
        &amp;lt;div class=&quot;hashtags&quot;&amp;gt;
            &amp;lt;!-- JavaScript로 처리하는 예시 (Thymeleaf에서는 복잡한 정규식 추출이 제한적) --&amp;gt;
            &amp;lt;span th:text=&quot;${post.content}&quot; style=&quot;display: none;&quot; th:attr=&quot;data-content=${post.content}&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;div id=&quot;extracted-hashtags&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 멘션 추출 --&amp;gt;
        &amp;lt;div class=&quot;mentions&quot;&amp;gt;
            &amp;lt;span th:if=&quot;${#strings.contains(post.content, '@')}&quot; class=&quot;has-mentions&quot;&amp;gt;
                이 게시글에 멘션이 포함되어 있습니다.
            &amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 코드 블록 감지 --&amp;gt;
    &amp;lt;div class=&quot;code-detection&quot; th:if=&quot;${comment.text}&quot;&amp;gt;
        &amp;lt;div th:if=&quot;${#strings.contains(comment.text, '```') or #strings.contains(comment.text, '&amp;lt;code&amp;gt;')}&quot;
             class=&quot;code-comment&quot;&amp;gt;
              코드가 포함된 댓글
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 마크다운 링크 감지 --&amp;gt;
    &amp;lt;div class=&quot;markdown-detection&quot; th:if=&quot;${document.content}&quot;&amp;gt;
        &amp;lt;span th:if=&quot;${#strings.matches(document.content, '.*\\[.*\\]\\(.*\\).*')}&quot; class=&quot;has-links&quot;&amp;gt;
              링크 포함
        &amp;lt;/span&amp;gt;

        &amp;lt;span th:if=&quot;${#strings.matches(document.content, '.*!\\[.*\\]\\(.*\\).*')}&quot; class=&quot;has-images&quot;&amp;gt;
             ️ 이미지 포함
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 금지어 필터링 --&amp;gt;
    &amp;lt;div class=&quot;content-filter&quot; th:with=&quot;bannedWords=${'욕설1|욕설2|금지어'}&quot;&amp;gt;
        &amp;lt;div th:if=&quot;${#strings.matches(userInput, '.*(' + bannedWords + ').*')}&quot; class=&quot;content-warning&quot;&amp;gt;
            ⚠️ 부적절한 내용이 감지되었습니다.
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
// 해시태그 추출을 위한 JavaScript (Thymeleaf 보완)
document.addEventListener('DOMContentLoaded', function() {
    const contentElement = document.querySelector('[data-content]');
    if (contentElement) {
        const content = contentElement.getAttribute('data-content');
        const hashtags = content.match(/#[a-zA-Z0-9가-힣_]+/g);
        const hashtagContainer = document.getElementById('extracted-hashtags');

        if (hashtags &amp;amp;&amp;amp; hashtagContainer) {
            hashtags.forEach(tag =&amp;gt; {
                const span = document.createElement('span');
                span.className = 'hashtag';
                span.textContent = tag;
                hashtagContainer.appendChild(span);
            });
        }
    }
});
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;실용적인 정규식 활용 예제&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;practical-regex-examples&quot;&amp;gt;
    &amp;lt;!-- 파일 확장자 검사 --&amp;gt;
    &amp;lt;div class=&quot;file-upload-validation&quot; th:each=&quot;file : ${uploadedFiles}&quot;&amp;gt;
        &amp;lt;div th:switch=&quot;${true}&quot;&amp;gt;
            &amp;lt;span th:case=&quot;${#strings.matches(file.name, '.*\\.(jpg|jpeg|png|gif))}&quot; class=&quot;image-file&quot;&amp;gt;
                 ️ &amp;lt;span th:text=&quot;${file.name}&quot;&amp;gt;이미지&amp;lt;/span&amp;gt;
            &amp;lt;/span&amp;gt;
            &amp;lt;span th:case=&quot;${#strings.matches(file.name, '.*\\.(pdf|doc|docx))}&quot; class=&quot;document-file&quot;&amp;gt;
                  &amp;lt;span th:text=&quot;${file.name}&quot;&amp;gt;문서&amp;lt;/span&amp;gt;
            &amp;lt;/span&amp;gt;
            &amp;lt;span th:case=&quot;${#strings.matches(file.name, '.*\\.(mp4|avi|mov))}&quot; class=&quot;video-file&quot;&amp;gt;
                  &amp;lt;span th:text=&quot;${file.name}&quot;&amp;gt;비디오&amp;lt;/span&amp;gt;
            &amp;lt;/span&amp;gt;
            &amp;lt;span th:case=&quot;*&quot; class=&quot;other-file&quot;&amp;gt;
                  &amp;lt;span th:text=&quot;${file.name}&quot;&amp;gt;기타&amp;lt;/span&amp;gt;
            &amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 버전 번호 비교 --&amp;gt;
    &amp;lt;div class=&quot;version-check&quot; th:with=&quot;currentVersion=${'1.2.3'}, requiredVersion=${'1.2.0'}&quot;&amp;gt;
        &amp;lt;p th:if=&quot;${#strings.matches(currentVersion, '^[0-9]+\\.[0-9]+\\.[0-9]+)}&quot; class=&quot;valid-version&quot;&amp;gt;
            현재 버전: &amp;lt;span th:text=&quot;${currentVersion}&quot;&amp;gt;1.2.3&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- HTML 태그 제거 검증 --&amp;gt;
    &amp;lt;div class=&quot;html-sanitization&quot; th:if=&quot;${userInput}&quot;&amp;gt;
        &amp;lt;div th:if=&quot;${#strings.matches(userInput, '.*&amp;lt;[^&amp;gt;]+&amp;gt;.*')}&quot; class=&quot;html-detected&quot;&amp;gt;
            ⚠️ HTML 태그가 감지되었습니다. 텍스트만 입력해주세요.
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 색상 코드 검증 --&amp;gt;
    &amp;lt;div class=&quot;color-validation&quot; th:if=&quot;${themeForm.primaryColor}&quot;&amp;gt;
        &amp;lt;div th:if=&quot;${#strings.matches(themeForm.primaryColor, '^#[0-9A-Fa-f]{6})}&quot; 
             th:style=&quot;|background-color: ${themeForm.primaryColor}; color: white; padding: 5px;|&quot;&amp;gt;
            유효한 색상 코드: &amp;lt;span th:text=&quot;${themeForm.primaryColor}&quot;&amp;gt;#FF5733&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 시간 형식 검증 --&amp;gt;
    &amp;lt;div class=&quot;time-validation&quot; th:if=&quot;${scheduleForm.startTime}&quot;&amp;gt;
        &amp;lt;span th:if=&quot;${#strings.matches(scheduleForm.startTime, '^([01]?[0-9]|2[0-3]):[0-5][0-9])}&quot; class=&quot;valid&quot;&amp;gt;
            ⏰ 시작 시간: &amp;lt;span th:text=&quot;${scheduleForm.startTime}&quot;&amp;gt;14:30&amp;lt;/span&amp;gt;
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 4장에서는 Thymeleaf의 표현식을 더욱 깊이 있게 다루었습니다. 조건 연산자, Elvis 연산자, 문자열 처리, 산술 연산, 논리 연산, 그리고 정규 표현식까지 실무에서 자주 사용되는 고급 기능들을 살펴보았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 요약&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;조건 연산자&lt;/b&gt;: 삼항 연산자를 통한 간단한 조건 분기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Elvis 연산자&lt;/b&gt;: null 안전한 기본값 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;No-Operation 토큰&lt;/b&gt;: 조건부 속성 설정 시 유용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문자열 연산&lt;/b&gt;: 동적 문자열 생성과 검증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;산술/논리 연산&lt;/b&gt;: 복잡한 비즈니스 로직 구현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정규 표현식&lt;/b&gt;: 입력 검증과 텍스트 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 고급 표현식들을 잘 활용하면 더욱 동적이고 유연한 템플릿을 만들 수 있습니다. 다음 장에서는 Thymeleaf의 표준 속성들에 대해 상세히 알아보겠습니다.&lt;/p&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/142</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-4-%ED%91%9C%EC%A4%80-%ED%91%9C%ED%98%84%EC%8B%9D-%EC%8B%AC%ED%99%94#entry142comment</comments>
      <pubDate>Mon, 25 Aug 2025 11:46:34 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #3.기본 문법과 표현식</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-3%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95%EA%B3%BC-%ED%91%9C%ED%98%84%EC%8B%9D</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;4109&quot; data-origin-height=&quot;833&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEUJTE/btsP46WNftq/xFsKlTni2Mg34QkyPL3ks0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEUJTE/btsP46WNftq/xFsKlTni2Mg34QkyPL3ks0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEUJTE/btsP46WNftq/xFsKlTni2Mg34QkyPL3ks0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEUJTE%2FbtsP46WNftq%2FxFsKlTni2Mg34QkyPL3ks0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;484&quot; height=&quot;98&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;4109&quot; data-origin-height=&quot;833&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3장. 기본 문법과 표현식&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Thymeleaf 네임스페이스와 속성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf는 HTML 태그에 &lt;code&gt;th:&lt;/code&gt; 접두사를 사용하여 동적 기능을 추가합니다. 이를 위해 HTML 문서의 루트 요소에 Thymeleaf 네임스페이스를 선언해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;u&gt;네임스페이스 선언&lt;/u&gt;&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title th:text=&quot;${pageTitle}&quot;&amp;gt;Default Title&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1 th:text=&quot;${welcomeMessage}&quot;&amp;gt;Welcome!&amp;lt;/h1&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;xmlns:th=&quot;http://www.thymeleaf.org&quot;&lt;/code&gt;: Thymeleaf 네임스페이스 선언&lt;/li&gt;
&lt;li&gt;이 선언 없이도 Thymeleaf는 작동하지만, IDE의 자동완성과 문법 검사를 위해 권장&lt;/li&gt;
&lt;li&gt;HTML5 유효성 검사를 통과하며 브라우저에서 무시됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;주요 Thymeleaf 속성 개요&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;attribute-examples&quot;&amp;gt;
    &amp;lt;!-- 텍스트 설정 --&amp;gt;
    &amp;lt;p th:text=&quot;${message}&quot;&amp;gt;기본 텍스트&amp;lt;/p&amp;gt;
    &amp;lt;p th:utext=&quot;${htmlContent}&quot;&amp;gt;기본 HTML&amp;lt;/p&amp;gt;

    &amp;lt;!-- 속성 설정 --&amp;gt;
    &amp;lt;input th:value=&quot;${userName}&quot; th:placeholder=&quot;${userHint}&quot;&amp;gt;
    &amp;lt;img th:src=&quot;@{/images/{filename}(filename=${user.avatar})}&quot; th:alt=&quot;${user.name}&quot;&amp;gt;

    &amp;lt;!-- 조건부 렌더링 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.isActive}&quot;&amp;gt;활성 사용자&amp;lt;/div&amp;gt;
    &amp;lt;div th:unless=&quot;${user.isActive}&quot;&amp;gt;비활성 사용자&amp;lt;/div&amp;gt;

    &amp;lt;!-- 반복 --&amp;gt;
    &amp;lt;ul&amp;gt;
        &amp;lt;li th:each=&quot;item : ${items}&quot; th:text=&quot;${item.name}&quot;&amp;gt;아이템&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;

    &amp;lt;!-- 객체 선택 --&amp;gt;
    &amp;lt;div th:object=&quot;${user}&quot;&amp;gt;
        &amp;lt;span th:text=&quot;*{name}&quot;&amp;gt;사용자명&amp;lt;/span&amp;gt;
        &amp;lt;span th:text=&quot;*{email}&quot;&amp;gt;이메일&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Variable Expressions (&lt;code&gt;${...}&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Variable Expression은 컨텍스트에서 변수를 참조하는 가장 기본적인 표현식입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;u&gt;기본 사용법&lt;/u&gt;&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 컨트롤러에서 전달된 데이터 --&amp;gt;
&amp;lt;div class=&quot;user-info&quot;&amp;gt;
    &amp;lt;h2 th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/h2&amp;gt;
    &amp;lt;p th:text=&quot;${user.email}&quot;&amp;gt;이메일&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.age}&quot;&amp;gt;나이&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 컬렉션 크기 --&amp;gt;
&amp;lt;p&amp;gt;총 &amp;lt;span th:text=&quot;${#lists.size(products)}&quot;&amp;gt;0&amp;lt;/span&amp;gt;개의 상품이 있습니다.&amp;lt;/p&amp;gt;

&amp;lt;!-- 조건부 표현 --&amp;gt;
&amp;lt;p th:text=&quot;${user.age &amp;gt;= 18} ? '성인' : '미성년자'&quot;&amp;gt;연령 구분&amp;lt;/p&amp;gt;

&amp;lt;!-- null 체크와 기본값 --&amp;gt;
&amp;lt;p th:text=&quot;${user.nickname != null} ? ${user.nickname} : ${user.name}&quot;&amp;gt;표시명&amp;lt;/p&amp;gt;

&amp;lt;!-- Elvis 연산자 사용 --&amp;gt;
&amp;lt;p th:text=&quot;${user.nickname ?: user.name}&quot;&amp;gt;표시명&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨트롤러 예제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Controller
public class UserController {

    @GetMapping(&quot;/user&quot;)
    public String userPage(Model model) {
        User user = new User(&quot;김철수&quot;, &quot;kim@example.com&quot;, 25);
        user.setNickname(&quot;철수&quot;);

        List&amp;lt;Product&amp;gt; products = Arrays.asList(
            new Product(&quot;노트북&quot;, 1200000),
            new Product(&quot;마우스&quot;, 30000),
            new Product(&quot;키보드&quot;, 80000)
        );

        model.addAttribute(&quot;user&quot;, user);
        model.addAttribute(&quot;products&quot;, products);

        return &quot;user&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;중첩 객체와 메소드 호출&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;user-profile&quot;&amp;gt;
    &amp;lt;!-- 중첩 객체 접근 --&amp;gt;
    &amp;lt;h3 th:text=&quot;${user.profile.displayName}&quot;&amp;gt;프로필명&amp;lt;/h3&amp;gt;
    &amp;lt;p th:text=&quot;${user.address.city}&quot;&amp;gt;도시&amp;lt;/p&amp;gt;

    &amp;lt;!-- 메소드 호출 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.getFullName()}&quot;&amp;gt;전체 이름&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.isAdult()}&quot;&amp;gt;성인 여부&amp;lt;/p&amp;gt;

    &amp;lt;!-- 파라미터가 있는 메소드 호출 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.getFormattedAge('년')}&quot;&amp;gt;포맷된 나이&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.hasPermission('WRITE')}&quot;&amp;gt;쓰기 권한&amp;lt;/p&amp;gt;

    &amp;lt;!-- 컬렉션 인덱스 접근 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.hobbies[0]}&quot;&amp;gt;첫 번째 취미&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.scores['math']}&quot;&amp;gt;수학 점수&amp;lt;/p&amp;gt;

    &amp;lt;!-- Safe Navigation (null-safe) --&amp;gt;
    &amp;lt;p th:text=&quot;${user.profile?.bio}&quot;&amp;gt;프로필 소개&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.address?.street?.toUpperCase()}&quot;&amp;gt;주소 (대문자)&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Java 모델 예제:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;public class User {
    private String firstName;
    private String lastName;
    private int age;
    private Profile profile;
    private Address address;
    private List&amp;lt;String&amp;gt; hobbies;
    private Map&amp;lt;String, Integer&amp;gt; scores;

    public String getFullName() {
        return firstName + &quot; &quot; + lastName;
    }

    public boolean isAdult() {
        return age &amp;gt;= 18;
    }

    public String getFormattedAge(String suffix) {
        return age + suffix;
    }

    public boolean hasPermission(String permission) {
        return profile != null &amp;amp;&amp;amp; profile.getPermissions().contains(permission);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;컬렉션과 배열 처리&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;collections-example&quot;&amp;gt;
    &amp;lt;!-- 리스트 접근 --&amp;gt;
    &amp;lt;h4&amp;gt;상품 목록&amp;lt;/h4&amp;gt;
    &amp;lt;div th:each=&quot;product, stat : ${products}&quot;&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;span th:text=&quot;${stat.count}&quot;&amp;gt;1&amp;lt;/span&amp;gt;. 
            &amp;lt;span th:text=&quot;${product.name}&quot;&amp;gt;상품명&amp;lt;/span&amp;gt; - 
            &amp;lt;span th:text=&quot;${#numbers.formatCurrency(product.price)}&quot;&amp;gt;가격&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 맵 접근 --&amp;gt;
    &amp;lt;h4&amp;gt;사용자 설정&amp;lt;/h4&amp;gt;
    &amp;lt;div th:each=&quot;setting : ${userSettings}&quot;&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;strong th:text=&quot;${setting.key}&quot;&amp;gt;설정명&amp;lt;/strong&amp;gt;: 
            &amp;lt;span th:text=&quot;${setting.value}&quot;&amp;gt;설정값&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 배열 길이와 비어있음 검사 --&amp;gt;
    &amp;lt;p th:if=&quot;${#arrays.isEmpty(user.tags)}&quot;&amp;gt;태그가 없습니다.&amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;${#arrays.isEmpty(user.tags)}&quot;&amp;gt;
        태그 개수: &amp;lt;span th:text=&quot;${#arrays.length(user.tags)}&quot;&amp;gt;0&amp;lt;/span&amp;gt;개
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 첫 번째와 마지막 요소 --&amp;gt;
    &amp;lt;p th:if=&quot;${!#lists.isEmpty(products)}&quot;&amp;gt;
        첫 상품: &amp;lt;span th:text=&quot;${products[0].name}&quot;&amp;gt;첫 상품&amp;lt;/span&amp;gt;&amp;lt;br&amp;gt;
        마지막 상품: &amp;lt;span th:text=&quot;${products[#lists.size(products) - 1].name}&quot;&amp;gt;마지막 상품&amp;lt;/span&amp;gt;
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 Selection Variable Expressions (&lt;code&gt;*{...}&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selection Variable Expression은 &lt;code&gt;th:object&lt;/code&gt;로 선택된 객체의 속성에 접근할 때 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 사용법&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;user-form&quot; th:object=&quot;${user}&quot;&amp;gt;
    &amp;lt;!-- *{property}는 ${user.property}와 동일 --&amp;gt;
    &amp;lt;h2 th:text=&quot;*{name}&quot;&amp;gt;사용자명&amp;lt;/h2&amp;gt;
    &amp;lt;p&amp;gt;이메일: &amp;lt;span th:text=&quot;*{email}&quot;&amp;gt;이메일&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;나이: &amp;lt;span th:text=&quot;*{age}&quot;&amp;gt;나이&amp;lt;/span&amp;gt; 세&amp;lt;/p&amp;gt;

    &amp;lt;!-- 중첩된 객체도 사용 가능 --&amp;gt;
    &amp;lt;div th:object=&quot;*{profile}&quot;&amp;gt;
        &amp;lt;p&amp;gt;닉네임: &amp;lt;span th:text=&quot;*{nickname}&quot;&amp;gt;닉네임&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;소개: &amp;lt;span th:text=&quot;*{bio}&quot;&amp;gt;소개&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;폼 바인딩에서의 활용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;form th:action=&quot;@{/user/update}&quot; th:object=&quot;${userForm}&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label for=&quot;name&quot;&amp;gt;이름&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;name&quot; th:field=&quot;*{name}&quot; th:value=&quot;*{name}&quot;&amp;gt;
        &amp;lt;span th:if=&quot;${#fields.hasErrors('name')}&quot; th:errors=&quot;*{name}&quot; class=&quot;error&quot;&amp;gt;이름 오류&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label for=&quot;email&quot;&amp;gt;이메일&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; th:field=&quot;*{email}&quot;&amp;gt;
        &amp;lt;span th:if=&quot;${#fields.hasErrors('email')}&quot; th:errors=&quot;*{email}&quot; class=&quot;error&quot;&amp;gt;이메일 오류&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label for=&quot;age&quot;&amp;gt;나이&amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;number&quot; id=&quot;age&quot; th:field=&quot;*{age}&quot; min=&quot;1&quot; max=&quot;120&quot;&amp;gt;
        &amp;lt;span th:if=&quot;${#fields.hasErrors('age')}&quot; th:errors=&quot;*{age}&quot; class=&quot;error&quot;&amp;gt;나이 오류&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 체크박스 --&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;
            &amp;lt;input type=&quot;checkbox&quot; th:field=&quot;*{newsletter}&quot;&amp;gt;
            뉴스레터 수신 동의
        &amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 라디오 버튼 --&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label&amp;gt;성별:&amp;lt;/label&amp;gt;
        &amp;lt;label&amp;gt;&amp;lt;input type=&quot;radio&quot; th:field=&quot;*{gender}&quot; value=&quot;M&quot;&amp;gt; 남성&amp;lt;/label&amp;gt;
        &amp;lt;label&amp;gt;&amp;lt;input type=&quot;radio&quot; th:field=&quot;*{gender}&quot; value=&quot;F&quot;&amp;gt; 여성&amp;lt;/label&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 선택 박스 --&amp;gt;
    &amp;lt;div class=&quot;form-group&quot;&amp;gt;
        &amp;lt;label for=&quot;role&quot;&amp;gt;역할&amp;lt;/label&amp;gt;
        &amp;lt;select id=&quot;role&quot; th:field=&quot;*{role}&quot;&amp;gt;
            &amp;lt;option value=&quot;&quot;&amp;gt;선택해주세요&amp;lt;/option&amp;gt;
            &amp;lt;option th:each=&quot;roleOption : ${roleOptions}&quot; 
                    th:value=&quot;${roleOption.value}&quot; 
                    th:text=&quot;${roleOption.label}&quot;&amp;gt;역할&amp;lt;/option&amp;gt;
        &amp;lt;/select&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;button type=&quot;submit&quot;&amp;gt;저장&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨트롤러에서의 폼 처리:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@PostMapping(&quot;/user/update&quot;)
public String updateUser(@ModelAttribute @Valid UserForm userForm, 
                        BindingResult bindingResult, 
                        Model model) {
    if (bindingResult.hasErrors()) {
        model.addAttribute(&quot;roleOptions&quot;, getRoleOptions());
        return &quot;user-form&quot;;
    }

    // 업데이트 로직
    userService.updateUser(userForm);
    return &quot;redirect:/user/&quot; + userForm.getId();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Selection Expression과 Variable Expression 혼용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div th:object=&quot;${order}&quot;&amp;gt;
    &amp;lt;h3&amp;gt;주문 정보&amp;lt;/h3&amp;gt;
    &amp;lt;p&amp;gt;주문번호: &amp;lt;span th:text=&quot;*{orderNumber}&quot;&amp;gt;12345&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;주문일자: &amp;lt;span th:text=&quot;*{orderDate}&quot;&amp;gt;2024-01-01&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- Variable Expression과 혼용 --&amp;gt;
    &amp;lt;p&amp;gt;주문자: &amp;lt;span th:text=&quot;${currentUser.name}&quot;&amp;gt;홍길동&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;총액: &amp;lt;span th:text=&quot;*{totalAmount}&quot;&amp;gt;50000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;

    &amp;lt;!-- 중첩된 Selection --&amp;gt;
    &amp;lt;div class=&quot;shipping-info&quot; th:object=&quot;*{shippingAddress}&quot;&amp;gt;
        &amp;lt;h4&amp;gt;배송 정보&amp;lt;/h4&amp;gt;
        &amp;lt;p&amp;gt;수령인: &amp;lt;span th:text=&quot;*{recipientName}&quot;&amp;gt;수령인&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;주소: &amp;lt;span th:text=&quot;*{fullAddress}&quot;&amp;gt;주소&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;전화번호: &amp;lt;span th:text=&quot;*{phoneNumber}&quot;&amp;gt;전화번호&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 주문 상품 목록 --&amp;gt;
    &amp;lt;div class=&quot;order-items&quot;&amp;gt;
        &amp;lt;h4&amp;gt;주문 상품&amp;lt;/h4&amp;gt;
        &amp;lt;div th:each=&quot;item : *{orderItems}&quot;&amp;gt;
            &amp;lt;div class=&quot;order-item&quot; th:object=&quot;${item}&quot;&amp;gt;
                &amp;lt;span th:text=&quot;*{productName}&quot;&amp;gt;상품명&amp;lt;/span&amp;gt; - 
                &amp;lt;span th:text=&quot;*{quantity}&quot;&amp;gt;1&amp;lt;/span&amp;gt;개 &amp;times; 
                &amp;lt;span th:text=&quot;*{unitPrice}&quot;&amp;gt;10000&amp;lt;/span&amp;gt;원 = 
                &amp;lt;span th:text=&quot;*{totalPrice}&quot;&amp;gt;10000&amp;lt;/span&amp;gt;원
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.4 Message Expressions (&lt;code&gt;#{...}&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Message Expression은 국제화(i18n) 메시지를 처리할 때 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;메시지 파일 설정&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# messages.properties (기본)
app.title=Thymeleaf 데모 애플리케이션
user.name=이름
user.email=이메일
user.age=나이
welcome.message=안녕하세요, {0}님!
item.count=총 {0}개의 항목이 있습니다.
validation.required={0}은(는) 필수 입력 항목입니다.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# messages_en.properties (영어)
app.title=Thymeleaf Demo Application
user.name=Name
user.email=Email
user.age=Age
welcome.message=Hello, {0}!
item.count=There are {0} items in total.
validation.required={0} is required field.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# messages_ja.properties (일본어)
app.title=Thymeleaf デモアプリケーション
user.name=名前
user.email=メール
user.age=年齢
welcome.message=こんにちは、{0}さん！
item.count=合計{0}個のアイテムがあります。
validation.required={0}は必須項目です。&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 메시지 사용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title th:text=&quot;#{app.title}&quot;&amp;gt;기본 제목&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1 th:text=&quot;#{app.title}&quot;&amp;gt;애플리케이션 제목&amp;lt;/h1&amp;gt;

    &amp;lt;form class=&quot;user-form&quot;&amp;gt;
        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label th:text=&quot;#{user.name}&quot; for=&quot;name&quot;&amp;gt;이름&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;text&quot; id=&quot;name&quot; name=&quot;name&quot;&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label th:text=&quot;#{user.email}&quot; for=&quot;email&quot;&amp;gt;이메일&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;email&quot; id=&quot;email&quot; name=&quot;email&quot;&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;form-group&quot;&amp;gt;
            &amp;lt;label th:text=&quot;#{user.age}&quot; for=&quot;age&quot;&amp;gt;나이&amp;lt;/label&amp;gt;
            &amp;lt;input type=&quot;number&quot; id=&quot;age&quot; name=&quot;age&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;!-- 파라미터가 있는 메시지 --&amp;gt;
    &amp;lt;p th:text=&quot;#{welcome.message(${user.name})}&quot;&amp;gt;환영 메시지&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;#{item.count(${#lists.size(items)})}&quot;&amp;gt;아이템 개수&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;복합 메시지와 조건부 메시지&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;message-examples&quot;&amp;gt;
    &amp;lt;!-- 단순 메시지 --&amp;gt;
    &amp;lt;h2 th:text=&quot;#{page.header}&quot;&amp;gt;페이지 헤더&amp;lt;/h2&amp;gt;

    &amp;lt;!-- 파라미터 치환 --&amp;gt;
    &amp;lt;p th:text=&quot;#{user.greeting(${user.name}, ${user.age})}&quot;&amp;gt;
        안녕하세요, 홍길동님! (25세)
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건부 메시지 --&amp;gt;
    &amp;lt;p th:text=&quot;#{${user.gender == 'M'} ? 'message.male' : 'message.female'}&quot;&amp;gt;
        성별에 따른 메시지
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 메시지와 변수 혼용 --&amp;gt;
    &amp;lt;p&amp;gt;
        &amp;lt;span th:text=&quot;#{status.label}&quot;&amp;gt;상태&amp;lt;/span&amp;gt;: 
        &amp;lt;span th:text=&quot;${user.isActive()} ? #{status.active} : #{status.inactive}&quot;&amp;gt;활성&amp;lt;/span&amp;gt;
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 복잡한 메시지 구성 --&amp;gt;
    &amp;lt;div class=&quot;notification&quot; th:with=&quot;count=${#lists.size(notifications)}&quot;&amp;gt;
        &amp;lt;span th:switch=&quot;${count}&quot;&amp;gt;
            &amp;lt;span th:case=&quot;0&quot; th:text=&quot;#{notification.none}&quot;&amp;gt;알림 없음&amp;lt;/span&amp;gt;
            &amp;lt;span th:case=&quot;1&quot; th:text=&quot;#{notification.single}&quot;&amp;gt;알림 1개&amp;lt;/span&amp;gt;
            &amp;lt;span th:case=&quot;*&quot; th:text=&quot;#{notification.multiple(${count})}&quot;&amp;gt;알림 여러개&amp;lt;/span&amp;gt;
        &amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;메시지 파일의 고급 기능&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 메시지 그룹화
button.save=저장
button.cancel=취소
button.delete=삭제
button.edit=편집

# HTML 내용이 포함된 메시지
message.html=&amp;lt;strong&amp;gt;중요:&amp;lt;/strong&amp;gt; 이 작업은 &amp;lt;em&amp;gt;되돌릴 수 없습니다&amp;lt;/em&amp;gt;.
message.link=자세한 내용은 &amp;lt;a href=&quot;/help&quot;&amp;gt;도움말&amp;lt;/a&amp;gt;을 참조하세요.

# 조건부 메시지
error.validation.required={0}을(를) 입력해주세요.
error.validation.email.invalid=올바른 이메일 주소를 입력해주세요.
error.validation.age.range=나이는 {0}세에서 {1}세 사이여야 합니다.

# 복수형 처리
item.count.zero=상품이 없습니다.
item.count.one=상품 1개가 있습니다.
item.count.many=상품 {0}개가 있습니다.&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- HTML 내용이 포함된 메시지 사용 --&amp;gt;
&amp;lt;div th:utext=&quot;#{message.html}&quot;&amp;gt;HTML 메시지&amp;lt;/div&amp;gt;
&amp;lt;div th:utext=&quot;#{message.link}&quot;&amp;gt;링크 메시지&amp;lt;/div&amp;gt;

&amp;lt;!-- 복수형 처리 --&amp;gt;
&amp;lt;p th:switch=&quot;${itemCount}&quot;&amp;gt;
    &amp;lt;span th:case=&quot;0&quot; th:text=&quot;#{item.count.zero}&quot;&amp;gt;상품 없음&amp;lt;/span&amp;gt;
    &amp;lt;span th:case=&quot;1&quot; th:text=&quot;#{item.count.one}&quot;&amp;gt;상품 1개&amp;lt;/span&amp;gt;
    &amp;lt;span th:case=&quot;*&quot; th:text=&quot;#{item.count.many(${itemCount})}&quot;&amp;gt;상품 여러개&amp;lt;/span&amp;gt;
&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.5 Link URL Expressions (&lt;code&gt;@{...}&lt;/code&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Link URL Expression은 URL을 생성할 때 사용하며, 컨텍스트 패스 자동 추가, 파라미터 처리 등의 기능을 제공합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;기본 URL 생성&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;nav class=&quot;main-nav&quot;&amp;gt;
    &amp;lt;!-- 절대 경로 --&amp;gt;
    &amp;lt;a th:href=&quot;@{/}&quot;&amp;gt;홈&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/users}&quot;&amp;gt;사용자 목록&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/products}&quot;&amp;gt;상품 목록&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/about}&quot;&amp;gt;소개&amp;lt;/a&amp;gt;

    &amp;lt;!-- 상대 경로 --&amp;gt;
    &amp;lt;a th:href=&quot;@{users/profile}&quot;&amp;gt;프로필&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{../admin}&quot;&amp;gt;관리자&amp;lt;/a&amp;gt;

    &amp;lt;!-- 외부 URL --&amp;gt;
    &amp;lt;a th:href=&quot;@{https://www.example.com}&quot;&amp;gt;외부 사이트&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{//cdn.example.com/assets/style.css}&quot;&amp;gt;CDN 리소스&amp;lt;/a&amp;gt;
&amp;lt;/nav&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;경로 변수와 쿼리 파라미터&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;user-links&quot;&amp;gt;
    &amp;lt;!-- 경로 변수 (Path Variable) --&amp;gt;
    &amp;lt;a th:href=&quot;@{/user/{id}(id=${user.id})}&quot;&amp;gt;사용자 상세&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/user/{id}/edit(id=${user.id})}&quot;&amp;gt;사용자 편집&amp;lt;/a&amp;gt;

    &amp;lt;!-- 여러 경로 변수 --&amp;gt;
    &amp;lt;a th:href=&quot;@{/category/{catId}/product/{prodId}(catId=${category.id}, prodId=${product.id})}&quot;&amp;gt;
        상품 상세
    &amp;lt;/a&amp;gt;

    &amp;lt;!-- 쿼리 파라미터 --&amp;gt;
    &amp;lt;a th:href=&quot;@{/search(q=${searchQuery})}&quot;&amp;gt;검색 결과&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/users(page=${currentPage + 1}, size=10)}&quot;&amp;gt;다음 페이지&amp;lt;/a&amp;gt;

    &amp;lt;!-- 경로 변수와 쿼리 파라미터 혼용 --&amp;gt;
    &amp;lt;a th:href=&quot;@{/user/{id}(id=${user.id}, tab='profile')}&quot;&amp;gt;
        사용자 프로필 탭
    &amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/category/{catId}/products(catId=${category.id}, sort='price', order='asc')}&quot;&amp;gt;
        가격순 정렬
    &amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;폼 액션과 리소스 URL&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 폼 액션 URL --&amp;gt;
&amp;lt;form th:action=&quot;@{/user/create}&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;이름&quot;&amp;gt;
    &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;이메일&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;생성&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;form th:action=&quot;@{/user/{id}/update(id=${user.id})}&quot; method=&quot;post&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;put&quot;&amp;gt;
    &amp;lt;input type=&quot;text&quot; th:value=&quot;${user.name}&quot; name=&quot;name&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;업데이트&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;!-- 삭제 폼 --&amp;gt;
&amp;lt;form th:action=&quot;@{/user/{id}/delete(id=${user.id})}&quot; method=&quot;post&quot; 
      onsubmit=&quot;return confirm('정말 삭제하시겠습니까?')&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; name=&quot;_method&quot; value=&quot;delete&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot; class=&quot;btn-danger&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;정적 리소스 URL&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;head&amp;gt;
    &amp;lt;!-- CSS 파일 --&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/main.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;

    &amp;lt;!-- JavaScript 파일 --&amp;gt;
    &amp;lt;script th:src=&quot;@{/js/jquery.min.js}&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script th:src=&quot;@{/js/app.js}&quot;&amp;gt;&amp;lt;/script&amp;gt;

    &amp;lt;!-- 파비콘 --&amp;gt;
    &amp;lt;link th:href=&quot;@{/favicon.ico}&quot; rel=&quot;icon&quot; type=&quot;image/x-icon&quot;&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;
    &amp;lt;!-- 이미지 --&amp;gt;
    &amp;lt;img th:src=&quot;@{/images/logo.png}&quot; alt=&quot;로고&quot;&amp;gt;
    &amp;lt;img th:src=&quot;@{/images/users/{filename}(filename=${user.avatar})}&quot; 
         th:alt=&quot;${user.name}&quot;&amp;gt;

    &amp;lt;!-- 동적 이미지 URL --&amp;gt;
    &amp;lt;div th:each=&quot;product : ${products}&quot;&amp;gt;
        &amp;lt;img th:src=&quot;@{/images/products/{id}.jpg(id=${product.id})}&quot; 
             th:alt=&quot;${product.name}&quot;
             onerror=&quot;this.src='/images/no-image.png'&quot;&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- PDF, 다운로드 링크 --&amp;gt;
    &amp;lt;a th:href=&quot;@{/files/manual.pdf}&quot; target=&quot;_blank&quot;&amp;gt;사용자 매뉴얼&amp;lt;/a&amp;gt;
    &amp;lt;a th:href=&quot;@{/download/{fileId}(fileId=${file.id})}&quot;&amp;gt;파일 다운로드&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;조건부 URL과 동적 URL 생성&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-links&quot;&amp;gt;
    &amp;lt;!-- 조건부 URL --&amp;gt;
    &amp;lt;a th:href=&quot;@{${user.isAdmin()} ? '/admin/dashboard' : '/user/dashboard'}&quot;&amp;gt;
        대시보드
    &amp;lt;/a&amp;gt;

    &amp;lt;!-- 동적 기본 URL --&amp;gt;
    &amp;lt;a th:href=&quot;@{${baseUrl} + '/api/data'}&quot; th:if=&quot;${baseUrl}&quot;&amp;gt;API 데이터&amp;lt;/a&amp;gt;

    &amp;lt;!-- 페이지네이션 --&amp;gt;
    &amp;lt;div class=&quot;pagination&quot;&amp;gt;
        &amp;lt;a th:if=&quot;${currentPage &amp;gt; 0}&quot; 
           th:href=&quot;@{/products(page=${currentPage - 1}, size=${pageSize}, sort=${sortBy})}&quot;&amp;gt;
            이전
        &amp;lt;/a&amp;gt;

        &amp;lt;span th:each=&quot;pageNum : ${#numbers.sequence(0, totalPages - 1)}&quot; 
              th:if=&quot;${pageNum &amp;gt;= currentPage - 2 and pageNum &amp;lt;= currentPage + 2}&quot;&amp;gt;
            &amp;lt;a th:if=&quot;${pageNum != currentPage}&quot;
               th:href=&quot;@{/products(page=${pageNum}, size=${pageSize}, sort=${sortBy})}&quot;
               th:text=&quot;${pageNum + 1}&quot;&amp;gt;1&amp;lt;/a&amp;gt;
            &amp;lt;span th:if=&quot;${pageNum == currentPage}&quot; 
                  th:text=&quot;${pageNum + 1}&quot; class=&quot;current-page&quot;&amp;gt;1&amp;lt;/span&amp;gt;
        &amp;lt;/span&amp;gt;

        &amp;lt;a th:if=&quot;${currentPage &amp;lt; totalPages - 1}&quot; 
           th:href=&quot;@{/products(page=${currentPage + 1}, size=${pageSize}, sort=${sortBy})}&quot;&amp;gt;
            다음
        &amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 정렬 링크 --&amp;gt;
    &amp;lt;div class=&quot;sort-options&quot;&amp;gt;
        &amp;lt;a th:href=&quot;@{/products(sort='name', order='asc', page=0)}&quot;
           th:class=&quot;${sortBy == 'name' and order == 'asc'} ? 'active' : ''&quot;&amp;gt;
            이름 &amp;uarr;
        &amp;lt;/a&amp;gt;
        &amp;lt;a th:href=&quot;@{/products(sort='name', order='desc', page=0)}&quot;
           th:class=&quot;${sortBy == 'name' and order == 'desc'} ? 'active' : ''&quot;&amp;gt;
            이름 &amp;darr;
        &amp;lt;/a&amp;gt;
        &amp;lt;a th:href=&quot;@{/products(sort='price', order='asc', page=0)}&quot;
           th:class=&quot;${sortBy == 'price' and order == 'asc'} ? 'active' : ''&quot;&amp;gt;
            가격 &amp;uarr;
        &amp;lt;/a&amp;gt;
        &amp;lt;a th:href=&quot;@{/products(sort='price', order='desc', page=0)}&quot;
           th:class=&quot;${sortBy == 'price' and order == 'desc'} ? 'active' : ''&quot;&amp;gt;
            가격 &amp;darr;
        &amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.6 Fragment Expressions (&lt;code&gt;~{...}&lt;/code&gt;)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fragment Expression은 템플릿 프래그먼트를 참조할 때 사용합니다. 템플릿의 재사용성을 높이는 중요한 기능입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;프래그먼트 정의&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- fragments/common.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;

&amp;lt;!-- 헤더 프래그먼트 --&amp;gt;
&amp;lt;div th:fragment=&quot;header&quot; class=&quot;header&quot;&amp;gt;
    &amp;lt;nav class=&quot;navbar&quot;&amp;gt;
        &amp;lt;a th:href=&quot;@{/}&quot; class=&quot;logo&quot;&amp;gt;MyApp&amp;lt;/a&amp;gt;
        &amp;lt;ul class=&quot;nav-links&quot;&amp;gt;
            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/}&quot;&amp;gt;홈&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/products}&quot;&amp;gt;상품&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
            &amp;lt;li&amp;gt;&amp;lt;a th:href=&quot;@{/about}&quot;&amp;gt;소개&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
        &amp;lt;/ul&amp;gt;
    &amp;lt;/nav&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 파라미터가 있는 프래그먼트 --&amp;gt;
&amp;lt;div th:fragment=&quot;userCard(user, showDetails)&quot; class=&quot;user-card&quot;&amp;gt;
    &amp;lt;h3 th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/h3&amp;gt;
    &amp;lt;p th:text=&quot;${user.email}&quot;&amp;gt;이메일&amp;lt;/p&amp;gt;
    &amp;lt;div th:if=&quot;${showDetails}&quot;&amp;gt;
        &amp;lt;p&amp;gt;나이: &amp;lt;span th:text=&quot;${user.age}&quot;&amp;gt;25&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;역할: &amp;lt;span th:text=&quot;${user.role}&quot;&amp;gt;user&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 푸터 프래그먼트 --&amp;gt;
&amp;lt;footer th:fragment=&quot;footer&quot; class=&quot;footer&quot;&amp;gt;
    &amp;lt;p&amp;gt;&amp;amp;copy; 2024 MyApp. All rights reserved.&amp;lt;/p&amp;gt;
    &amp;lt;div class=&quot;social-links&quot;&amp;gt;
        &amp;lt;a href=&quot;#&quot; class=&quot;social-link&quot;&amp;gt;Facebook&amp;lt;/a&amp;gt;
        &amp;lt;a href=&quot;#&quot; class=&quot;social-link&quot;&amp;gt;Twitter&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/footer&amp;gt;

&amp;lt;!-- 메타 태그 프래그먼트 --&amp;gt;
&amp;lt;head th:fragment=&quot;meta(title, description)&quot; th:remove=&quot;tag&quot;&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title th:text=&quot;${title}&quot;&amp;gt;기본 제목&amp;lt;/title&amp;gt;
    &amp;lt;meta name=&quot;description&quot; th:content=&quot;${description}&quot;&amp;gt;
    &amp;lt;link th:href=&quot;@{/css/main.css}&quot; rel=&quot;stylesheet&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;프래그먼트 사용&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;!-- 메타 프래그먼트 삽입 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: meta('상품 목록', '다양한 상품을 만나보세요')}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!-- 헤더 프래그먼트 삽입 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: header}&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;main class=&quot;main-content&quot;&amp;gt;
        &amp;lt;h1&amp;gt;상품 목록&amp;lt;/h1&amp;gt;

        &amp;lt;!-- 사용자 카드 프래그먼트 반복 사용 --&amp;gt;
        &amp;lt;div class=&quot;users-grid&quot;&amp;gt;
            &amp;lt;div th:each=&quot;user : ${users}&quot;&amp;gt;
                &amp;lt;div th:replace=&quot;~{fragments/common :: userCard(${user}, true)}&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 단순한 사용자 목록 (세부정보 없이) --&amp;gt;
        &amp;lt;div class=&quot;simple-users&quot;&amp;gt;
            &amp;lt;div th:each=&quot;user : ${simpleUsers}&quot;&amp;gt;
                &amp;lt;div th:replace=&quot;~{fragments/common :: userCard(${user}, false)}&quot;&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/main&amp;gt;

    &amp;lt;!-- 푸터 프래그먼트 삽입 --&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/common :: footer}&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Insert, Replace, Include의 차이&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- fragments/demo.html --&amp;gt;
&amp;lt;div th:fragment=&quot;content&quot; class=&quot;demo-content&quot;&amp;gt;
    &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 사용 예제 --&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;th:insert 사용&amp;lt;/h2&amp;gt;
    &amp;lt;div th:insert=&quot;~{fragments/demo :: content}&quot;&amp;gt;
        기존 내용은 유지됩니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;h2&amp;gt;th:replace 사용&amp;lt;/h2&amp;gt;
    &amp;lt;div th:replace=&quot;~{fragments/demo :: content}&quot;&amp;gt;
        이 내용은 완전히 교체됩니다.
    &amp;lt;/div&amp;gt;

    &amp;lt;h2&amp;gt;th:include 사용 (Deprecated)&amp;lt;/h2&amp;gt;
    &amp;lt;div th:include=&quot;~{fragments/demo :: content}&quot;&amp;gt;
        프래그먼트의 내용만 포함됩니다.
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;렌더링 결과:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;body&amp;gt;
    &amp;lt;h2&amp;gt;th:insert 사용&amp;lt;/h2&amp;gt;
    &amp;lt;div&amp;gt;
        기존 내용은 유지됩니다.
        &amp;lt;div class=&quot;demo-content&quot;&amp;gt;
            &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;h2&amp;gt;th:replace 사용&amp;lt;/h2&amp;gt;
    &amp;lt;div class=&quot;demo-content&quot;&amp;gt;
        &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;h2&amp;gt;th:include 사용&amp;lt;/h2&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;p&amp;gt;이것은 프래그먼트 내용입니다.&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.7 리터럴과 텍스트 연산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf에서 다양한 리터럴 타입과 텍스트 연산을 사용할 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;텍스트 리터럴&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;literals-example&quot;&amp;gt;
    &amp;lt;!-- 문자열 템플릿 (Literal substitutions) --&amp;gt;
    &amp;lt;p th:text=&quot;|안녕하세요, ${user.name}님! 나이: ${user.age}세|&quot;&amp;gt;템플릿 문자열&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;|오늘은 ${#dates.format(#dates.createNow(), 'yyyy-MM-dd')}입니다.|&quot;&amp;gt;날짜 포함&amp;lt;/p&amp;gt;

    &amp;lt;!-- 숫자 리터럴 --&amp;gt;
    &amp;lt;p th:text=&quot;42&quot;&amp;gt;숫자&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;3.14159&quot;&amp;gt;소수&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.age + 10}&quot;&amp;gt;나이 + 10&amp;lt;/p&amp;gt;

    &amp;lt;!-- 불리언 리터럴 --&amp;gt;
    &amp;lt;p th:text=&quot;true&quot;&amp;gt;참&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;false&quot;&amp;gt;거짓&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;true&quot;&amp;gt;항상 표시&amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;false&quot;&amp;gt;항상 표시&amp;lt;/p&amp;gt;

    &amp;lt;!-- null 리터럴 --&amp;gt;
    &amp;lt;p th:text=&quot;null&quot;&amp;gt;null 값&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${user.nickname != null}&quot;&amp;gt;닉네임이 있음&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;문자열 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;string-operations&quot;&amp;gt;
    &amp;lt;!-- 문자열 연결 --&amp;gt;
    &amp;lt;p th:text=&quot;'Hello' + ' ' + 'World'&quot;&amp;gt;Hello World&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.firstName} + ' ' + ${user.lastName}&quot;&amp;gt;전체 이름&amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 템플릿 사용 (권장) --&amp;gt;
    &amp;lt;p th:text=&quot;|${user.firstName} ${user.lastName}|&quot;&amp;gt;전체 이름&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;|이메일: ${user.email}, 나이: ${user.age}세|&quot;&amp;gt;사용자 정보&amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건부 문자열 --&amp;gt;
    &amp;lt;p th:text=&quot;|상태: ${user.isActive() ? '활성' : '비활성'}|&quot;&amp;gt;상태&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;|권한: ${user.isAdmin() ? '관리자' : '일반 사용자'}|&quot;&amp;gt;권한&amp;lt;/p&amp;gt;

    &amp;lt;!-- HTML 속성에서의 문자열 템플릿 --&amp;gt;
    &amp;lt;img th:src=&quot;|/images/users/${user.id}.jpg|&quot; th:alt=&quot;|${user.name} 프로필|&quot;&amp;gt;
    &amp;lt;a th:href=&quot;|/user/${user.id}/edit|&quot; th:title=&quot;|${user.name} 편집|&quot;&amp;gt;편집&amp;lt;/a&amp;gt;

    &amp;lt;!-- 복잡한 문자열 구성 --&amp;gt;
    &amp;lt;p th:text=&quot;|${user.name}님의 마지막 로그인: ${#temporals.format(user.lastLogin, 'yyyy-MM-dd HH:mm')}|&quot;&amp;gt;
        마지막 로그인 정보
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;산술 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;arithmetic-operations&quot;&amp;gt;
    &amp;lt;!-- 기본 산술 연산 --&amp;gt;
    &amp;lt;p&amp;gt;덧셈: &amp;lt;span th:text=&quot;5 + 3&quot;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;뺄셈: &amp;lt;span th:text=&quot;10 - 4&quot;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;곱셈: &amp;lt;span th:text=&quot;6 * 7&quot;&amp;gt;42&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;나눗셈: &amp;lt;span th:text=&quot;15 / 3&quot;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;나머지: &amp;lt;span th:text=&quot;17 % 5&quot;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- 변수와의 연산 --&amp;gt;
    &amp;lt;p&amp;gt;현재 나이: &amp;lt;span th:text=&quot;${user.age}&quot;&amp;gt;25&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;10년 후: &amp;lt;span th:text=&quot;${user.age + 10}&quot;&amp;gt;35&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;태어난 년도: &amp;lt;span th:text=&quot;${#dates.year(#dates.createNow())} - ${user.age}&quot;&amp;gt;1999&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;

    &amp;lt;!-- 가격 계산 --&amp;gt;
    &amp;lt;div th:each=&quot;item : ${cartItems}&quot;&amp;gt;
        &amp;lt;p th:text=&quot;|${item.name}: ${item.quantity} &amp;times; ${item.price} = ${item.quantity * item.price}원|&quot;&amp;gt;
            상품: 2 &amp;times; 10000 = 20000원
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;p&amp;gt;총합: &amp;lt;span th:text=&quot;${#aggregates.sum(cartItems.![quantity * price])}&quot;&amp;gt;50000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;

    &amp;lt;!-- 할인 계산 --&amp;gt;
    &amp;lt;div th:if=&quot;${discount &amp;gt; 0}&quot;&amp;gt;
        &amp;lt;p&amp;gt;원가: &amp;lt;span th:text=&quot;${totalPrice}&quot;&amp;gt;50000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;할인율: &amp;lt;span th:text=&quot;${discount}&quot;&amp;gt;10&amp;lt;/span&amp;gt;%&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;할인금액: &amp;lt;span th:text=&quot;${totalPrice * discount / 100}&quot;&amp;gt;5000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;최종가격: &amp;lt;span th:text=&quot;${totalPrice * (100 - discount) / 100}&quot;&amp;gt;45000&amp;lt;/span&amp;gt;원&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;비교 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;comparison-operations&quot;&amp;gt;
    &amp;lt;!-- 숫자 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${user.age &amp;gt;= 18}&quot;&amp;gt;성인입니다.&amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;${user.age &amp;gt;= 18}&quot;&amp;gt;미성년자입니다.&amp;lt;/p&amp;gt;

    &amp;lt;p th:text=&quot;${user.score &amp;gt; 80} ? '우수' : (${user.score &amp;gt; 60} ? '보통' : '노력필요')&quot;&amp;gt;성적&amp;lt;/p&amp;gt;

    &amp;lt;!-- 문자열 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${user.role == 'admin'}&quot;&amp;gt;관리자 권한&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${user.name != null and user.name != ''}&quot;&amp;gt;이름이 설정됨&amp;lt;/p&amp;gt;

    &amp;lt;!-- 객체 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${currentUser == user}&quot;&amp;gt;현재 로그인한 사용자&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${currentUser.id == user.id}&quot;&amp;gt;같은 사용자&amp;lt;/p&amp;gt;

    &amp;lt;!-- 컬렉션 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${#lists.size(user.orders) &amp;gt; 0}&quot;&amp;gt;주문 내역이 있습니다.&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${#lists.isEmpty(user.orders)}&quot;&amp;gt;주문 내역이 없습니다.&amp;lt;/p&amp;gt;

    &amp;lt;!-- 날짜 비교 --&amp;gt;
    &amp;lt;p th:if=&quot;${user.createdAt &amp;gt; #dates.createNow().minusDays(30)}&quot;&amp;gt;최근 가입 사용자&amp;lt;/p&amp;gt;

    &amp;lt;!-- 범위 비교 --&amp;gt;
    &amp;lt;div th:switch=&quot;true&quot;&amp;gt;
        &amp;lt;p th:case=&quot;${user.age &amp;lt; 20}&quot;&amp;gt;10대&amp;lt;/p&amp;gt;
        &amp;lt;p th:case=&quot;${user.age &amp;lt; 30}&quot;&amp;gt;20대&amp;lt;/p&amp;gt;
        &amp;lt;p th:case=&quot;${user.age &amp;lt; 40}&quot;&amp;gt;30대&amp;lt;/p&amp;gt;
        &amp;lt;p th:case=&quot;*&quot;&amp;gt;40대 이상&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;논리 연산&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;logical-operations&quot;&amp;gt;
    &amp;lt;!-- AND 연산 --&amp;gt;
    &amp;lt;p th:if=&quot;${user.isActive() and user.isVerified()}&quot;&amp;gt;활성화된 인증 사용자&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${user.age &amp;gt;= 18 and user.hasPermission('PURCHASE')}&quot;&amp;gt;구매 가능 사용자&amp;lt;/p&amp;gt;

    &amp;lt;!-- OR 연산 --&amp;gt;
    &amp;lt;p th:if=&quot;${user.isAdmin() or user.isModerator()}&quot;&amp;gt;관리 권한 사용자&amp;lt;/p&amp;gt;
    &amp;lt;p th:if=&quot;${user.isPremium() or user.score &amp;gt; 90}&quot;&amp;gt;특별 혜택 대상&amp;lt;/p&amp;gt;

    &amp;lt;!-- NOT 연산 --&amp;gt;
    &amp;lt;p th:if=&quot;${not user.isActive()}&quot;&amp;gt;비활성 사용자&amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;${user.isActive()}&quot;&amp;gt;비활성 사용자&amp;lt;/p&amp;gt; &amp;lt;!-- 위와 동일 --&amp;gt;

    &amp;lt;!-- 복합 논리 연산 --&amp;gt;
    &amp;lt;div th:if=&quot;${(user.isActive() and user.isVerified()) or user.isAdmin()}&quot;&amp;gt;
        &amp;lt;p&amp;gt;접근 허용&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 삼항 연산자와 결합 --&amp;gt;
    &amp;lt;p th:text=&quot;${(user.isActive() and user.isVerified()) ? '정상 사용자' : '제한 사용자'}&quot;&amp;gt;
        사용자 상태
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 복잡한 조건 --&amp;gt;
    &amp;lt;div th:if=&quot;${user.role == 'admin' or (user.role == 'user' and user.score &amp;gt; 80)}&quot;&amp;gt;
        &amp;lt;p&amp;gt;고급 기능 사용 가능&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;조건 연산자와 Elvis 연산자&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;conditional-operations&quot;&amp;gt;
    &amp;lt;!-- 삼항 연산자 (조건 ? 참 : 거짓) --&amp;gt;
    &amp;lt;p th:text=&quot;${user.age &amp;gt;= 18} ? '성인' : '미성년자'&quot;&amp;gt;연령 분류&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.score &amp;gt;= 80} ? 'A' : (${user.score &amp;gt;= 60} ? 'B' : 'C')&quot;&amp;gt;등급&amp;lt;/p&amp;gt;

    &amp;lt;!-- Elvis 연산자 (값 ?: 기본값) --&amp;gt;
    &amp;lt;p th:text=&quot;${user.nickname ?: user.name}&quot;&amp;gt;표시 이름&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.bio ?: '소개가 없습니다.'}&quot;&amp;gt;자기소개&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.profileImage ?: '/images/default-avatar.png'}&quot;&amp;gt;프로필 이미지&amp;lt;/p&amp;gt;

    &amp;lt;!-- null 안전 연산자와 결합 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.profile?.nickname ?: user.name}&quot;&amp;gt;안전한 닉네임&amp;lt;/p&amp;gt;
    &amp;lt;p th:text=&quot;${user.company?.name ?: '소속 없음'}&quot;&amp;gt;회사명&amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건 연산자 중첩 --&amp;gt;
    &amp;lt;span th:class=&quot;${user.role == 'admin'} ? 'badge-admin' : 
                   (${user.role == 'moderator'} ? 'badge-mod' : 'badge-user')&quot;
          th:text=&quot;${user.role}&quot;&amp;gt;역할&amp;lt;/span&amp;gt;

    &amp;lt;!-- 복잡한 Elvis 연산자 사용 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.settings?.displayName ?: user.profile?.nickname ?: user.name}&quot;&amp;gt;
        우선순위별 이름 표시
    &amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;No-Operation 토큰&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;no-operation-examples&quot;&amp;gt;
    &amp;lt;!-- _ (언더스코어)는 아무 작업도 하지 않음 --&amp;gt;
    &amp;lt;p th:text=&quot;${user.isActive()} ? ${user.name} : _&quot;&amp;gt;
        활성 사용자일 때만 이름 표시
    &amp;lt;/p&amp;gt;

    &amp;lt;!-- 조건부 속성 설정 --&amp;gt;
    &amp;lt;div th:class=&quot;${user.isActive()} ? 'active-user' : _&quot;
         th:attr=&quot;data-user-id=${user.isActive()} ? ${user.id} : _&quot;&amp;gt;
        조건부 속성
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 선택적 링크 --&amp;gt;
    &amp;lt;a th:href=&quot;${user.hasProfile()} ? @{/user/{id}/profile(id=${user.id})} : _&quot;
       th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/a&amp;gt;

    &amp;lt;!-- 배경 이미지 조건부 설정 --&amp;gt;
    &amp;lt;div th:style=&quot;${user.coverImage} ? |background-image: url(${user.coverImage})| : _&quot;
         class=&quot;user-header&quot;&amp;gt;
        사용자 헤더
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;종합 예제: 사용자 카드 컴포넌트&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;user-card&quot; th:each=&quot;user : ${users}&quot;&amp;gt;
    &amp;lt;!-- 사용자 기본 정보 --&amp;gt;
    &amp;lt;div class=&quot;user-avatar&quot;&amp;gt;
        &amp;lt;img th:src=&quot;${user.avatar ?: '/images/default-avatar.png'}&quot; 
             th:alt=&quot;|${user.name} 아바타|&quot;&amp;gt;

        &amp;lt;!-- 온라인 상태 표시 --&amp;gt;
        &amp;lt;span th:if=&quot;${user.isOnline()}&quot; class=&quot;status-online&quot; title=&quot;온라인&quot;&amp;gt;&amp;lt;/span&amp;gt;
        &amp;lt;span th:unless=&quot;${user.isOnline()}&quot; class=&quot;status-offline&quot; title=&quot;오프라인&quot;&amp;gt;&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;user-info&quot;&amp;gt;
        &amp;lt;!-- 이름과 닉네임 --&amp;gt;
        &amp;lt;h3 th:text=&quot;${user.nickname ?: user.name}&quot;&amp;gt;사용자명&amp;lt;/h3&amp;gt;
        &amp;lt;p class=&quot;user-email&quot; th:text=&quot;${user.email}&quot;&amp;gt;이메일&amp;lt;/p&amp;gt;

        &amp;lt;!-- 역할 배지 --&amp;gt;
        &amp;lt;span th:class=&quot;|badge badge-${user.role}|&quot; 
              th:text=&quot;${user.role == 'admin'} ? '관리자' : 
                       (${user.role == 'moderator'} ? '운영자' : '사용자')&quot;&amp;gt;역할&amp;lt;/span&amp;gt;

        &amp;lt;!-- 가입일 표시 --&amp;gt;
        &amp;lt;p class=&quot;join-date&quot;&amp;gt;
            &amp;lt;span th:text=&quot;|가입일: ${#temporals.format(user.createdAt, 'yyyy-MM-dd')}|&quot;&amp;gt;가입일&amp;lt;/span&amp;gt;
            &amp;lt;span th:if=&quot;${#temporals.daysBetween(user.createdAt, #temporals.createNow())} &amp;lt; 30&quot; 
                  class=&quot;new-member&quot;&amp;gt;NEW&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;

        &amp;lt;!-- 점수와 등급 --&amp;gt;
        &amp;lt;div class=&quot;user-score&quot; th:if=&quot;${user.score != null}&quot;&amp;gt;
            &amp;lt;span&amp;gt;점수: &amp;lt;/span&amp;gt;
            &amp;lt;span th:text=&quot;${user.score}&quot;&amp;gt;85&amp;lt;/span&amp;gt;
            &amp;lt;span th:text=&quot;'(' + (${user.score &amp;gt;= 90} ? 'S' : 
                                  ${user.score &amp;gt;= 80} ? 'A' : 
                                  ${user.score &amp;gt;= 70} ? 'B' : 'C') + '등급)'&quot;&amp;gt;
                (A등급)
            &amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 활동 통계 --&amp;gt;
        &amp;lt;div class=&quot;user-stats&quot; th:if=&quot;${not #lists.isEmpty(user.activities)}&quot;&amp;gt;
            &amp;lt;small&amp;gt;
                &amp;lt;span th:text=&quot;|게시글 ${#lists.size(user.posts)}개|&quot;&amp;gt;게시글 5개&amp;lt;/span&amp;gt; | 
                &amp;lt;span th:text=&quot;|댓글 ${#lists.size(user.comments)}개|&quot;&amp;gt;댓글 12개&amp;lt;/span&amp;gt;
            &amp;lt;/small&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 액션 버튼 --&amp;gt;
    &amp;lt;div class=&quot;user-actions&quot;&amp;gt;
        &amp;lt;a th:href=&quot;@{/user/{id}(id=${user.id})}&quot; class=&quot;btn btn-primary&quot;&amp;gt;프로필&amp;lt;/a&amp;gt;

        &amp;lt;!-- 관리자만 볼 수 있는 버튼 --&amp;gt;
        &amp;lt;div th:if=&quot;${currentUser.isAdmin()}&quot;&amp;gt;
            &amp;lt;button th:onclick=&quot;|toggleUserStatus(${user.id})|&quot; 
                    th:class=&quot;${user.isActive()} ? 'btn btn-warning' : 'btn btn-success'&quot;
                    th:text=&quot;${user.isActive()} ? '비활성화' : '활성화'&quot;&amp;gt;
                상태 변경
            &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 메시지 전송 (자신이 아닐 때만) --&amp;gt;
        &amp;lt;a th:unless=&quot;${currentUser.id == user.id}&quot;
           th:href=&quot;@{/messages/new(to=${user.id})}&quot; 
           class=&quot;btn btn-secondary&quot;&amp;gt;메시지&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 최근 활동 --&amp;gt;
    &amp;lt;div class=&quot;recent-activity&quot; th:if=&quot;${user.lastActivity != null}&quot;&amp;gt;
        &amp;lt;small th:text=&quot;|최근 활동: ${#temporals.formatISO(user.lastActivity)}|&quot;&amp;gt;
            최근 활동: 2024-01-15T14:30:00
        &amp;lt;/small&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 3장에서는 Thymeleaf의 5가지 핵심 표현식과 다양한 연산자들을 상세히 살펴보았습니다. 각 표현식의 특징과 사용 사례를 이해하면 더욱 효과적으로 Thymeleaf를 활용할 수 있습니다.&lt;/p&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/141</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-3%EA%B8%B0%EB%B3%B8-%EB%AC%B8%EB%B2%95%EA%B3%BC-%ED%91%9C%ED%98%84%EC%8B%9D#entry141comment</comments>
      <pubDate>Mon, 25 Aug 2025 11:40:03 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #2. 개발환경설정</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-2-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZepgd/btsP4hkgU6x/WJXiOI5mQxlFBSSq2Vma30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZepgd/btsP4hkgU6x/WJXiOI5mQxlFBSSq2Vma30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZepgd/btsP4hkgU6x/WJXiOI5mQxlFBSSq2Vma30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZepgd%2FbtsP4hkgU6x%2FWJXiOI5mQxlFBSSq2Vma30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;&lt;b&gt;2장. 개발 환경 설정&lt;/b&gt;&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.1 시스템 요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf를 사용하기 위한 기본 요구사항은 다음과 같습니다:&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;필수 요구사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Java&lt;/b&gt;: JDK 8 이상 (권장: JDK 11 또는 17)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thymeleaf&lt;/b&gt;: 3.0.x 이상 (권장: 3.1.x)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Boot&lt;/b&gt;: 2.7.x 이상 (권장: 3.x)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지원 환경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;웹 서버&lt;/b&gt;: Tomcat, Jetty, Undertow&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌드 도구&lt;/b&gt;: Maven 3.6+, Gradle 6.0+&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IDE&lt;/b&gt;: IntelliJ IDEA, Eclipse, Visual Studio Code&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;버전 호환성 매트릭스&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Thymeleaf&lt;/th&gt;
&lt;th&gt;Spring Boot&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3.1.x&lt;/td&gt;
&lt;td&gt;3.0+&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;td&gt;최신 권장 조합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.0.x&lt;/td&gt;
&lt;td&gt;2.7+&lt;/td&gt;
&lt;td&gt;11+&lt;/td&gt;
&lt;td&gt;안정적인 조합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.1.x&lt;/td&gt;
&lt;td&gt;1.5+&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;td&gt;레거시 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.2 Maven/Gradle 의존성 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Maven 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot Starter 사용 (권장)&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;

    &amp;lt;parent&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-boot-starter-parent&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.2.0&amp;lt;/version&amp;gt;
        &amp;lt;relativePath/&amp;gt;
    &amp;lt;/parent&amp;gt;

    &amp;lt;groupId&amp;gt;com.example&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;thymeleaf-demo&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.0-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;packaging&amp;gt;jar&amp;lt;/packaging&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;17&amp;lt;/java.version&amp;gt;
        &amp;lt;thymeleaf.version&amp;gt;3.1.2.RELEASE&amp;lt;/thymeleaf.version&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;!-- Thymeleaf Starter --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-thymeleaf&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- Web Starter --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- 개발 도구 (선택사항) --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-devtools&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
            &amp;lt;optional&amp;gt;true&amp;lt;/optional&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- 보안 (필요시) --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- Thymeleaf Security Integration --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.thymeleaf.extras&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;thymeleaf-extras-springsecurity6&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;!-- 테스트 의존성 --&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&amp;lt;/project&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;직접 의존성 관리&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
    &amp;lt;!-- Core Thymeleaf --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.thymeleaf&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;thymeleaf&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.1.2.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;

    &amp;lt;!-- Spring Integration --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.thymeleaf&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;thymeleaf-spring6&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.1.2.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;

    &amp;lt;!-- 추가 기능 --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.thymeleaf.extras&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;thymeleaf-extras-java8time&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.0.4.RELEASE&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;spring-boot-starter-thymeleaf&lt;/code&gt;: 필요한 모든 Thymeleaf 의존성을 자동으로 포함&lt;/li&gt;
&lt;li&gt;&lt;code&gt;spring-boot-devtools&lt;/code&gt;: 템플릿 변경 시 자동 재시작 및 핫 리로드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thymeleaf-extras-springsecurity6&lt;/code&gt;: Spring Security와의 통합 지원&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thymeleaf-extras-java8time&lt;/code&gt;: Java 8+ 시간 API 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gradle 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kotlin DSL&lt;/h4&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;plugins {
    id(&quot;org.springframework.boot&quot;) version &quot;3.2.0&quot;
    id(&quot;io.spring.dependency-management&quot;) version &quot;1.1.0&quot;
    kotlin(&quot;jvm&quot;) version &quot;1.9.0&quot;
    kotlin(&quot;plugin.spring&quot;) version &quot;1.9.0&quot;
}

group = &quot;com.example&quot;
version = &quot;1.0-SNAPSHOT&quot;
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    // Thymeleaf
    implementation(&quot;org.springframework.boot:spring-boot-starter-thymeleaf&quot;)
    implementation(&quot;org.springframework.boot:spring-boot-starter-web&quot;)

    // 보안
    implementation(&quot;org.springframework.boot:spring-boot-starter-security&quot;)
    implementation(&quot;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&quot;)

    // 개발 도구
    developmentOnly(&quot;org.springframework.boot:spring-boot-devtools&quot;)

    // 테스트
    testImplementation(&quot;org.springframework.boot:spring-boot-starter-test&quot;)
    testImplementation(&quot;org.springframework.security:spring-security-test&quot;)
}

tasks.withType&amp;lt;Test&amp;gt; {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Groovy DSL&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;plugins {
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'com.example'
version = '1.0-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

    developmentOnly 'org.springframework.boot:spring-boot-devtools'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.3 Spring Boot 프로젝트 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.yml 설정&lt;/h3&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;# 기본 서버 설정
server:
  port: 8080
  servlet:
    context-path: /

# Thymeleaf 설정
spring:
  thymeleaf:
    # 템플릿 모드 설정
    mode: HTML
    # 템플릿 파일 위치
    prefix: classpath:/templates/
    suffix: .html
    # 개발 시 캐시 비활성화
    cache: false
    # 인코딩 설정
    encoding: UTF-8
    # 템플릿이 존재하지 않을 때 예외 발생 여부
    check-template: true
    check-template-location: true
    # 렌더링 전 템플릿 존재 확인
    enable-spring-el-compiler: true
    # 템플릿 해결 순서
    template-resolver-order: 1
    # 뷰 이름 패턴 (선택적)
    view-names: 
      - &quot;admin/*&quot;
      - &quot;user/*&quot;
    # 제외할 뷰 이름 패턴 (선택적)
    excluded-view-names:
      - &quot;error/*&quot;

  # 웹 설정
  web:
    resources:
      # 정적 리소스 캐싱 (개발 시 비활성화)
      cache:
        cachecontrol:
          max-age: 0
      # 정적 리소스 위치
      static-locations: classpath:/static/

  # 메시지 소스 설정 (국제화)
  messages:
    basename: messages/messages
    encoding: UTF-8
    cache-duration: -1  # 개발 시 캐시 비활성화

# 로깅 설정
logging:
  level:
    org.thymeleaf: DEBUG  # 개발 시에만 사용
    org.springframework.web: DEBUG
    root: INFO

# 프로파일별 설정
---
spring:
  config:
    activate:
      on-profile: development

  thymeleaf:
    cache: false  # 개발 환경에서 캐시 비활성화

  devtools:
    restart:
      enabled: true
    livereload:
      enabled: true

---
spring:
  config:
    activate:
      on-profile: production

  thymeleaf:
    cache: true  # 프로덕션에서 캐시 활성화

logging:
  level:
    org.thymeleaf: WARN
    org.springframework.web: WARN&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설정 항목 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;cache: false&lt;/code&gt;: 개발 중 템플릿 변경사항이 즉시 반영되도록 캐시 비활성화&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode: HTML&lt;/code&gt;: HTML5 템플릿 모드 (기본값)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;check-template-location&lt;/code&gt;: 템플릿 파일 존재 여부 확인&lt;/li&gt;
&lt;li&gt;&lt;code&gt;enable-spring-el-compiler&lt;/code&gt;: Spring EL 컴파일러 활성화로 성능 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;application.properties 설정 (대안)&lt;/h3&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 서버 설정
server.port=8080

# Thymeleaf 기본 설정
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false
spring.thymeleaf.check-template-location=true

# 정적 리소스 설정
spring.web.resources.static-locations=classpath:/static/
spring.web.resources.cache.cachecontrol.max-age=0

# 메시지 소스
spring.messages.basename=messages/messages
spring.messages.encoding=UTF-8

# 개발 도구
spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true

# 로깅
logging.level.org.thymeleaf=DEBUG
logging.level.org.springframework.web=DEBUG&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java Configuration&lt;/h3&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;

@Configuration
public class ThymeleafConfig implements WebMvcConfigurer {

    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = 
            new SpringResourceTemplateResolver();

        templateResolver.setPrefix(&quot;classpath:/templates/&quot;);
        templateResolver.setSuffix(&quot;.html&quot;);
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCharacterEncoding(&quot;UTF-8&quot;);
        templateResolver.setCacheable(false); // 개발 환경
        templateResolver.setOrder(1);

        return templateResolver;
    }

    @Bean
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver());
        templateEngine.setEnableSpringELCompiler(true);

        // 추가 dialect 등록 (필요시)
        // templateEngine.addDialect(new CustomDialect());

        return templateEngine;
    }

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setCharacterEncoding(&quot;UTF-8&quot;);
        viewResolver.setOrder(1);

        return viewResolver;
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(viewResolver());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Java Configuration 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;SpringResourceTemplateResolver&lt;/code&gt;: Spring의 리소스 로딩 메커니즘 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SpringTemplateEngine&lt;/code&gt;: Thymeleaf 템플릿 엔진 설정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThymeleafViewResolver&lt;/code&gt;: Spring MVC와의 통합을 위한 뷰 리졸버&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.4 IDE 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IntelliJ IDEA 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;플러그인 설치&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;File &amp;rarr; Settings &amp;rarr; Plugins&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thymeleaf&lt;/b&gt; 플러그인 검색 후 설치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Boot&lt;/b&gt; 플러그인 확인 (보통 기본 설치됨)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로젝트 설정&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// IntelliJ IDEA 실행 설정
// Run/Debug Configurations &amp;rarr; Spring Boot &amp;rarr; VM options
-Dspring.profiles.active=development
-Dspring.devtools.restart.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Live Templates 설정&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- th:text --&amp;gt;
th:text=&quot;${$VAR$}&quot;

&amp;lt;!-- th:each --&amp;gt;
th:each=&quot;$ITEM$ : ${$COLLECTION$}&quot;

&amp;lt;!-- th:if --&amp;gt;
th:if=&quot;${$CONDITION$}&quot;

&amp;lt;!-- 완전한 Thymeleaf HTML 템플릿 --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title th:text=&quot;${title}&quot;&amp;gt;$TITLE$&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1 th:text=&quot;${heading}&quot;&amp;gt;$HEADING$&amp;lt;/h1&amp;gt;
    $END$
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Eclipse/STS 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Tools 4 설치&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Help &amp;rarr; Eclipse Marketplace&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Tools 4&lt;/b&gt; 검색 후 설치&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Thymeleaf Plugin&lt;/b&gt; 설치 (선택사항)&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로젝트 Import&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Eclipse 실행 설정
// Run Configurations &amp;rarr; Java Application &amp;rarr; Arguments tab &amp;rarr; VM arguments
-Dspring.profiles.active=development
-Dspring.devtools.restart.enabled=true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Visual Studio Code 설정&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;확장 프로그램 설치&lt;/h4&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;recommendations&quot;: [
    &quot;vscjava.vscode-spring-initializr&quot;,
    &quot;vscjava.vscode-spring-boot-dashboard&quot;,
    &quot;pivotal.vscode-spring-boot&quot;,
    &quot;wholroyd.jinja&quot;,
    &quot;ms-vscode.vscode-typescript-next&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;settings.json 설정&lt;/h4&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;java.configuration.updateBuildConfiguration&quot;: &quot;automatic&quot;,
  &quot;spring-boot.ls.problem.application-properties.unknown-property&quot;: &quot;ignore&quot;,
  &quot;files.associations&quot;: {
    &quot;*.html&quot;: &quot;html&quot;
  },
  &quot;emmet.includeLanguages&quot;: {
    &quot;thymeleaf&quot;: &quot;html&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.5 첫 번째 Thymeleaf 애플리케이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로젝트 구조&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;src/
├── main/
│   ├── java/
│   │   └── com/example/demo/
│   │       ├── DemoApplication.java
│   │       ├── controller/
│   │       │   └── HomeController.java
│   │       └── model/
│   │           └── User.java
│   └── resources/
│       ├── static/
│       │   ├── css/
│       │   │   └── style.css
│       │   └── js/
│       │       └── app.js
│       ├── templates/
│       │   ├── index.html
│       │   └── user/
│       │       └── profile.html
│       └── application.yml
└── test/
    └── java/
        └── com/example/demo/
            └── DemoApplicationTests.java&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메인 애플리케이션 클래스&lt;/h3&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모델 클래스&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;package com.example.demo.model;

import java.time.LocalDateTime;
import java.util.List;

public class User {
    private Long id;
    private String name;
    private String email;
    private String role;
    private boolean active;
    private LocalDateTime lastLogin;
    private List&amp;lt;String&amp;gt; permissions;

    // 생성자
    public User() {}

    public User(Long id, String name, String email, String role) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.role = role;
        this.active = true;
        this.lastLogin = LocalDateTime.now();
    }

    // Getter/Setter 메소드
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getRole() { return role; }
    public void setRole(String role) { this.role = role; }

    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }

    public LocalDateTime getLastLogin() { return lastLogin; }
    public void setLastLogin(LocalDateTime lastLogin) { this.lastLogin = lastLogin; }

    public List&amp;lt;String&amp;gt; getPermissions() { return permissions; }
    public void setPermissions(List&amp;lt;String&amp;gt; permissions) { this.permissions = permissions; }

    // 편의 메소드
    public String getDisplayName() {
        return name != null ? name : email;
    }

    public boolean isAdmin() {
        return &quot;admin&quot;.equalsIgnoreCase(role);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨트롤러&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;package com.example.demo.controller;

import com.example.demo.model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@Controller
public class HomeController {

    @GetMapping(&quot;/&quot;)
    public String home(Model model) {
        // 기본 데이터 설정
        model.addAttribute(&quot;title&quot;, &quot;Thymeleaf Demo Application&quot;);
        model.addAttribute(&quot;welcomeMessage&quot;, &quot;Welcome to Thymeleaf!&quot;);
        model.addAttribute(&quot;currentTime&quot;, LocalDateTime.now());

        // 사용자 데이터
        User currentUser = new User(1L, &quot;John Doe&quot;, &quot;john@example.com&quot;, &quot;admin&quot;);
        currentUser.setPermissions(Arrays.asList(&quot;READ&quot;, &quot;WRITE&quot;, &quot;DELETE&quot;));
        model.addAttribute(&quot;user&quot;, currentUser);

        // 사용자 목록
        List&amp;lt;User&amp;gt; users = Arrays.asList(
            new User(1L, &quot;John Doe&quot;, &quot;john@example.com&quot;, &quot;admin&quot;),
            new User(2L, &quot;Jane Smith&quot;, &quot;jane@example.com&quot;, &quot;user&quot;),
            new User(3L, &quot;Bob Johnson&quot;, &quot;bob@example.com&quot;, &quot;moderator&quot;)
        );
        model.addAttribute(&quot;users&quot;, users);

        return &quot;index&quot;;
    }

    @GetMapping(&quot;/user/{id}&quot;)
    public String userProfile(@PathVariable Long id, Model model) {
        // 실제로는 서비스에서 사용자 정보를 조회
        User user = new User(id, &quot;John Doe&quot;, &quot;john@example.com&quot;, &quot;admin&quot;);
        user.setLastLogin(LocalDateTime.now().minusDays(1));

        model.addAttribute(&quot;user&quot;, user);
        model.addAttribute(&quot;title&quot;, &quot;User Profile - &quot; + user.getName());

        return &quot;user/profile&quot;;
    }

    @GetMapping(&quot;/search&quot;)
    public String search(@RequestParam(required = false) String query, Model model) {
        model.addAttribute(&quot;title&quot;, &quot;Search Results&quot;);
        model.addAttribute(&quot;query&quot;, query);

        if (query != null &amp;amp;&amp;amp; !query.trim().isEmpty()) {
            // 검색 로직 (예시)
            List&amp;lt;User&amp;gt; searchResults = Arrays.asList(
                new User(1L, &quot;John Doe&quot;, &quot;john@example.com&quot;, &quot;admin&quot;)
            );
            model.addAttribute(&quot;results&quot;, searchResults);
            model.addAttribute(&quot;resultCount&quot;, searchResults.size());
        }

        return &quot;search&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메인 템플릿&lt;/h3&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title th:text=&quot;${title}&quot;&amp;gt;Thymeleaf Demo&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{/css/style.css}&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;!-- 헤더 --&amp;gt;
    &amp;lt;header class=&quot;header&quot;&amp;gt;
        &amp;lt;nav class=&quot;nav&quot;&amp;gt;
            &amp;lt;h1 th:text=&quot;${title}&quot;&amp;gt;Thymeleaf Demo Application&amp;lt;/h1&amp;gt;
            &amp;lt;div class=&quot;nav-links&quot;&amp;gt;
                &amp;lt;a th:href=&quot;@{/}&quot;&amp;gt;Home&amp;lt;/a&amp;gt;
                &amp;lt;a th:href=&quot;@{/search}&quot;&amp;gt;Search&amp;lt;/a&amp;gt;
                &amp;lt;span th:if=&quot;${user}&quot; class=&quot;user-info&quot;&amp;gt;
                    Welcome, &amp;lt;span th:text=&quot;${user.displayName}&quot;&amp;gt;User&amp;lt;/span&amp;gt;!
                    &amp;lt;a th:href=&quot;@{/user/{id}(id=${user.id})}&quot;&amp;gt;Profile&amp;lt;/a&amp;gt;
                &amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/nav&amp;gt;
    &amp;lt;/header&amp;gt;

    &amp;lt;!-- 메인 콘텐츠 --&amp;gt;
    &amp;lt;main class=&quot;main-content&quot;&amp;gt;
        &amp;lt;div class=&quot;welcome-section&quot;&amp;gt;
            &amp;lt;h2 th:text=&quot;${welcomeMessage}&quot;&amp;gt;Welcome Message&amp;lt;/h2&amp;gt;
            &amp;lt;p&amp;gt;Current time: &amp;lt;span th:text=&quot;${#temporals.format(currentTime, 'yyyy-MM-dd HH:mm:ss')}&quot;&amp;gt;2024-01-01 12:00:00&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 현재 사용자 정보 --&amp;gt;
        &amp;lt;section th:if=&quot;${user}&quot; class=&quot;user-section&quot;&amp;gt;
            &amp;lt;h3&amp;gt;Current User Information&amp;lt;/h3&amp;gt;
            &amp;lt;div class=&quot;user-card&quot;&amp;gt;
                &amp;lt;h4 th:text=&quot;${user.name}&quot;&amp;gt;John Doe&amp;lt;/h4&amp;gt;
                &amp;lt;p&amp;gt;Email: &amp;lt;span th:text=&quot;${user.email}&quot;&amp;gt;john@example.com&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;Role: &amp;lt;span th:text=&quot;${user.role}&quot; th:class=&quot;|role-${user.role}|&quot;&amp;gt;admin&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                &amp;lt;p&amp;gt;Status: 
                    &amp;lt;span th:text=&quot;${user.active} ? 'Active' : 'Inactive'&quot; 
                          th:class=&quot;${user.active} ? 'status-active' : 'status-inactive'&quot;&amp;gt;Active&amp;lt;/span&amp;gt;
                &amp;lt;/p&amp;gt;

                &amp;lt;!-- 권한 목록 --&amp;gt;
                &amp;lt;div th:if=&quot;${user.permissions}&quot; class=&quot;permissions&quot;&amp;gt;
                    &amp;lt;h5&amp;gt;Permissions:&amp;lt;/h5&amp;gt;
                    &amp;lt;ul&amp;gt;
                        &amp;lt;li th:each=&quot;permission : ${user.permissions}&quot; 
                            th:text=&quot;${permission}&quot;&amp;gt;READ&amp;lt;/li&amp;gt;
                    &amp;lt;/ul&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;

        &amp;lt;!-- 사용자 목록 --&amp;gt;
        &amp;lt;section th:if=&quot;${users}&quot; class=&quot;users-section&quot;&amp;gt;
            &amp;lt;h3&amp;gt;All Users (&amp;lt;span th:text=&quot;${#lists.size(users)}&quot;&amp;gt;0&amp;lt;/span&amp;gt;)&amp;lt;/h3&amp;gt;
            &amp;lt;div class=&quot;users-grid&quot;&amp;gt;
                &amp;lt;div th:each=&quot;u, stat : ${users}&quot; 
                     class=&quot;user-card&quot; 
                     th:classappend=&quot;${stat.odd} ? 'odd' : 'even'&quot;&amp;gt;
                    &amp;lt;div class=&quot;user-header&quot;&amp;gt;
                        &amp;lt;h4 th:text=&quot;${u.name}&quot;&amp;gt;User Name&amp;lt;/h4&amp;gt;
                        &amp;lt;span th:if=&quot;${u.admin}&quot; class=&quot;admin-badge&quot;&amp;gt;Admin&amp;lt;/span&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;p th:text=&quot;${u.email}&quot;&amp;gt;user@example.com&amp;lt;/p&amp;gt;
                    &amp;lt;div class=&quot;user-actions&quot;&amp;gt;
                        &amp;lt;a th:href=&quot;@{/user/{id}(id=${u.id})}&quot; class=&quot;btn btn-primary&quot;&amp;gt;View Profile&amp;lt;/a&amp;gt;
                        &amp;lt;span th:switch=&quot;${u.role}&quot;&amp;gt;
                            &amp;lt;button th:case=&quot;'admin'&quot; class=&quot;btn btn-danger&quot;&amp;gt;Admin Actions&amp;lt;/button&amp;gt;
                            &amp;lt;button th:case=&quot;'moderator'&quot; class=&quot;btn btn-warning&quot;&amp;gt;Mod Actions&amp;lt;/button&amp;gt;
                            &amp;lt;button th:case=&quot;*&quot; class=&quot;btn btn-secondary&quot;&amp;gt;User Actions&amp;lt;/button&amp;gt;
                        &amp;lt;/span&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/section&amp;gt;
    &amp;lt;/main&amp;gt;

    &amp;lt;!-- 푸터 --&amp;gt;
    &amp;lt;footer class=&quot;footer&quot;&amp;gt;
        &amp;lt;p&amp;gt;&amp;amp;copy; 2024 Thymeleaf Demo. Built with Spring Boot and Thymeleaf.&amp;lt;/p&amp;gt;
    &amp;lt;/footer&amp;gt;

    &amp;lt;script th:src=&quot;@{/js/app.js}&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSS 스타일&lt;/h3&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;/* src/main/resources/static/css/style.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    color: #333;
    background-color: #f8f9fa;
}

.header {
    background-color: #007bff;
    color: white;
    padding: 1rem 0;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.nav {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 2rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.nav h1 {
    font-size: 1.5rem;
    font-weight: 300;
}

.nav-links {
    display: flex;
    gap: 1rem;
    align-items: center;
}

.nav-links a {
    color: white;
    text-decoration: none;
    padding: 0.5rem 1rem;
    border-radius: 4px;
    transition: background-color 0.2s;
}

.nav-links a:hover {
    background-color: rgba(255,255,255,0.1);
}

.main-content {
    max-width: 1200px;
    margin: 2rem auto;
    padding: 0 2rem;
}

.welcome-section {
    background: white;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 2rem;
}

.user-section, .users-section {
    background: white;
    padding: 2rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    margin-bottom: 2rem;
}

.user-card {
    border: 1px solid #e9ecef;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1rem;
    background: #f8f9fa;
}

.users-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 1rem;
}

.user-card.odd {
    background-color: #f1f3f4;
}

.user-card.even {
    background-color: #ffffff;
}

.user-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
}

.admin-badge {
    background-color: #dc3545;
    color: white;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: bold;
}

.role-admin { color: #dc3545; font-weight: bold; }
.role-moderator { color: #fd7e14; font-weight: bold; }
.role-user { color: #6c757d; }

.status-active { color: #28a745; font-weight: bold; }
.status-inactive { color: #dc3545; font-weight: bold; }

.permissions ul {
    list-style: none;
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.permissions li {
    background-color: #e9ecef;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    font-size: 0.8rem;
}

.btn {
    display: inline-block;
    padding: 0.5rem 1rem;
    text-decoration: none;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    font-size: 0.9rem;
    transition: background-color 0.2s;
}

.btn-primary { background-color: #007bff; color: white; }
.btn-danger { background-color: #dc3545; color: white; }
.btn-warning { background-color: #ffc107; color: black; }
.btn-secondary { background-color: #6c757d; color: white; }

.btn:hover {
    opacity: 0.8;
}

.user-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 1rem;
}

.footer {
    background-color: #343a40;
    color: white;
    text-align: center;
    padding: 1rem 0;
    margin-top: 2rem;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JavaScript 파일&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/main/resources/static/js/app.js
document.addEventListener('DOMContentLoaded', function() {
    console.log('Thymeleaf Demo App loaded');

    // 사용자 카드 클릭 이벤트
    const userCards = document.querySelectorAll('.user-card');
    userCards.forEach(card =&amp;gt; {
        card.addEventListener('click', function(e) {
            if (!e.target.matches('a, button')) {
                this.classList.toggle('expanded');
            }
        });
    });

    // 현재 시간 업데이트
    updateCurrentTime();
    setInterval(updateCurrentTime, 1000);
});

function updateCurrentTime() {
    const timeElement = document.querySelector('.current-time');
    if (timeElement) {
        timeElement.textContent = new Date().toLocaleString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;애플리케이션 실행&lt;/h3&gt;
&lt;pre class=&quot;vala&quot;&gt;&lt;code&gt;# Maven 사용시
mvn spring-boot:run

# Gradle 사용시
./gradlew bootRun

# JAR 실행
java -jar target/demo-1.0-SNAPSHOT.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행 후 확인사항:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저에서 &lt;code&gt;http://localhost:8080&lt;/code&gt; 접속&lt;/li&gt;
&lt;li&gt;사용자 정보가 정상적으로 표시되는지 확인&lt;/li&gt;
&lt;li&gt;템플릿 수정 후 자동 리로드 작동 확인&lt;/li&gt;
&lt;li&gt;DevTools를 통한 핫 리로드 테스트&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 기본적인 Thymeleaf 애플리케이션이 완성되었습니다. 다음 장에서는 Thymeleaf의 문법과 표현식에 대해 자세히 알아보겠습니다.&lt;/p&gt;</description>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/140</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C-2-%EA%B0%9C%EB%B0%9C%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95#entry140comment</comments>
      <pubDate>Mon, 25 Aug 2025 11:29:47 +0900</pubDate>
    </item>
    <item>
      <title>Thymeleaf 가이드 - #1. Thymeleaf 소개</title>
      <link>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dge4Qz/btsP4S5goFD/qIvj55A0NbaY4dtzA0vkWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dge4Qz/btsP4S5goFD/qIvj55A0NbaY4dtzA0vkWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dge4Qz/btsP4S5goFD/qIvj55A0NbaY4dtzA0vkWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdge4Qz%2FbtsP4S5goFD%2FqIvj55A0NbaY4dtzA0vkWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;499&quot; height=&quot;101&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1장. Thymeleaf 소개&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Thymeleaf란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf는 웹 및 독립형 환경에서 사용할 수 있는 현대적인 서버사이드 Java 템플릿 엔진입니다. 2011년에 Daniel Fern&amp;aacute;ndez에 의해 개발되었으며, HTML, XML, JavaScript, CSS, 그리고 일반 텍스트를 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf의 핵심은 &lt;b&gt;&quot;Natural Templates&quot;&lt;/b&gt; 개념입니다. 이는 템플릿 파일이 웹 브라우저에서 직접 열어도 올바르게 표시되는 유효한 HTML 문서라는 의미입니다. 이러한 특성은 디자이너와 개발자 간의 협업을 크게 향상시킵니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Welcome Page&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1 th:text=&quot;${welcomeMessage}&quot;&amp;gt;Welcome to our website!&amp;lt;/h1&amp;gt;
    &amp;lt;p th:text=&quot;${description}&quot;&amp;gt;This is a sample description that shows in browser.&amp;lt;/p&amp;gt;

    &amp;lt;div th:if=&quot;${user}&quot;&amp;gt;
        &amp;lt;p&amp;gt;Hello, &amp;lt;span th:text=&quot;${user.name}&quot;&amp;gt;Guest User&amp;lt;/span&amp;gt;!&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;Your role: &amp;lt;span th:text=&quot;${user.role}&quot;&amp;gt;User&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div th:unless=&quot;${user}&quot;&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;a href=&quot;/login&quot;&amp;gt;Please log in&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;xmlns:th=&quot;http://www.thymeleaf.org&quot;&lt;/code&gt;: Thymeleaf 네임스페이스 선언&lt;/li&gt;
&lt;li&gt;&lt;code&gt;th:text=&quot;${welcomeMessage}&quot;&lt;/code&gt;: 서버에서 전달된 &lt;code&gt;welcomeMessage&lt;/code&gt; 변수의 값으로 텍스트 교체&lt;/li&gt;
&lt;li&gt;브라우저에서 직접 열면 &quot;Welcome to our website!&quot;가 표시되지만, 서버에서 렌더링되면 실제 변수 값으로 교체됩니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;th:if&lt;/code&gt;와 &lt;code&gt;th:unless&lt;/code&gt;를 통한 조건부 렌더링&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 주요 특징과 장점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Natural Templates (자연스러운 템플릿)&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 큰 특징으로, HTML 파일 자체가 완전히 유효한 HTML 문서입니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 일반적인 템플릿 엔진의 경우 --&amp;gt;
&amp;lt;p&amp;gt;Hello &amp;lt;%= user.getName() %&amp;gt;!&amp;lt;/p&amp;gt;  &amp;lt;!-- 브라우저에서 깨짐 --&amp;gt;

&amp;lt;!-- Thymeleaf의 경우 --&amp;gt;
&amp;lt;p th:text=&quot;|Hello ${user.name}!|&quot;&amp;gt;Hello Guest!&amp;lt;/p&amp;gt;  &amp;lt;!-- 브라우저에서 정상 표시 --&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 예제는 JSP 스타일로, 브라우저에서 직접 열면 스크립틀릿이 그대로 표시됩니다&lt;/li&gt;
&lt;li&gt;두 번째 예제는 Thymeleaf 방식으로, 브라우저에서는 &quot;Hello Guest!&quot;가 표시되고, 서버에서 렌더링되면 실제 사용자 이름이 표시됩니다&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;강력한 표현식 언어&lt;/u&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf는 Spring Expression Language(SpEL)를 기반으로 한 풍부한 표현식을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 기본 변수 표현식 --&amp;gt;
&amp;lt;p th:text=&quot;${user.name}&quot;&amp;gt;사용자명&amp;lt;/p&amp;gt;

&amp;lt;!-- 선택 변수 표현식 (객체 선택) --&amp;gt;
&amp;lt;div th:object=&quot;${user}&quot;&amp;gt;
    &amp;lt;p&amp;gt;Name: &amp;lt;span th:text=&quot;*{name}&quot;&amp;gt;John&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Email: &amp;lt;span th:text=&quot;*{email}&quot;&amp;gt;john@example.com&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Age: &amp;lt;span th:text=&quot;*{age}&quot;&amp;gt;25&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 링크 URL 표현식 --&amp;gt;
&amp;lt;a th:href=&quot;@{/users/{id}(id=${user.id})}&quot;&amp;gt;View Profile&amp;lt;/a&amp;gt;

&amp;lt;!-- 메시지 표현식 (국제화) --&amp;gt;
&amp;lt;p th:text=&quot;#{welcome.message}&quot;&amp;gt;Welcome message&amp;lt;/p&amp;gt;

&amp;lt;!-- 복합 표현식 --&amp;gt;
&amp;lt;p th:text=&quot;|Hello ${user.name}, you have ${user.messageCount} messages|&quot;&amp;gt;
    Hello John, you have 5 messages
&amp;lt;/p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;각 표현식 설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;${...}&lt;/code&gt;: 컨텍스트 변수에 접근&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*{...}&lt;/code&gt;: 선택된 객체의 속성에 접근 (&lt;code&gt;th:object&lt;/code&gt;와 함께 사용)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@{...}&lt;/code&gt;: URL 생성 (컨텍스트 경로 자동 추가, 파라미터 처리)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#{...}&lt;/code&gt;: 국제화 메시지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;|...|&lt;/code&gt;: 문자열 리터럴 템플릿 (문자열 연결 간소화)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;조건부 처리와 반복&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 조건부 렌더링 --&amp;gt;
&amp;lt;div th:if=&quot;${user.isActive()}&quot;&amp;gt;
    &amp;lt;p th:text=&quot;|Welcome back, ${user.name}!|&quot;&amp;gt;Welcome back, John!&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div th:unless=&quot;${user.isActive()}&quot;&amp;gt;
    &amp;lt;p class=&quot;warning&quot;&amp;gt;Your account is inactive.&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- Switch 문 --&amp;gt;
&amp;lt;div th:switch=&quot;${user.role}&quot;&amp;gt;
    &amp;lt;p th:case=&quot;'admin'&quot; class=&quot;admin&quot;&amp;gt;Administrator Access&amp;lt;/p&amp;gt;
    &amp;lt;p th:case=&quot;'moderator'&quot; class=&quot;mod&quot;&amp;gt;Moderator Access&amp;lt;/p&amp;gt;
    &amp;lt;p th:case=&quot;*&quot; class=&quot;user&quot;&amp;gt;Regular User Access&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 반복 처리 --&amp;gt;
&amp;lt;table&amp;gt;
    &amp;lt;tr th:each=&quot;product : ${products}&quot;&amp;gt;
        &amp;lt;td th:text=&quot;${product.name}&quot;&amp;gt;Product Name&amp;lt;/td&amp;gt;
        &amp;lt;td th:text=&quot;${product.price}&quot;&amp;gt;$99.99&amp;lt;/td&amp;gt;
        &amp;lt;td&amp;gt;
            &amp;lt;span th:if=&quot;${product.inStock}&quot; class=&quot;available&quot;&amp;gt;Available&amp;lt;/span&amp;gt;
            &amp;lt;span th:unless=&quot;${product.inStock}&quot; class=&quot;soldout&quot;&amp;gt;Sold Out&amp;lt;/span&amp;gt;
        &amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;

&amp;lt;!-- 반복 상태 변수 활용 --&amp;gt;
&amp;lt;ul&amp;gt;
    &amp;lt;li th:each=&quot;item, stat : ${items}&quot; 
        th:class=&quot;${stat.odd} ? 'odd-row' : 'even-row'&quot;
        th:text=&quot;|${stat.index + 1}. ${item.title}|&quot;&amp;gt;
        1. Sample Item
    &amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;th:if&lt;/code&gt;/&lt;code&gt;th:unless&lt;/code&gt;: 조건에 따른 요소 표시/숨김&lt;/li&gt;
&lt;li&gt;&lt;code&gt;th:switch&lt;/code&gt;/&lt;code&gt;th:case&lt;/code&gt;: 다중 조건 분기 (Java의 switch문과 유사)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;th:each&lt;/code&gt;: 컬렉션 반복, 상태 변수(&lt;code&gt;stat&lt;/code&gt;)로 인덱스, 카운트, 홀짝 여부 등 확인 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 다른 템플릿 엔진과의 비교&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Thymeleaf vs JSP&lt;/u&gt;&lt;/h4&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;특징&lt;/th&gt;
&lt;th&gt;Thymeleaf&lt;/th&gt;
&lt;th&gt;JSP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Natural Templates&lt;/td&gt;
&lt;td&gt;✅ 완전한 HTML&lt;/td&gt;
&lt;td&gt;❌ 스크립틀릿으로 인한 HTML 파괴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;브라우저 미리보기&lt;/td&gt;
&lt;td&gt;✅ 가능&lt;/td&gt;
&lt;td&gt;❌ 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디자이너 친화성&lt;/td&gt;
&lt;td&gt;✅ 매우 좋음&lt;/td&gt;
&lt;td&gt;❌ 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;⚠️ 상대적으로 느림&lt;/td&gt;
&lt;td&gt;✅ 빠름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;학습 곡선&lt;/td&gt;
&lt;td&gt;⚠️ 중간&lt;/td&gt;
&lt;td&gt;✅ Java 개발자에게 친숙&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- JSP 예제 --&amp;gt;
&amp;lt;%@ page contentType=&quot;text/html;charset=UTF-8&quot; language=&quot;java&quot; %&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;% if (user != null) { %&amp;gt;
        &amp;lt;p&amp;gt;Hello &amp;lt;%= user.getName() %&amp;gt;!&amp;lt;/p&amp;gt;
    &amp;lt;% } else { %&amp;gt;
        &amp;lt;p&amp;gt;Please log in&amp;lt;/p&amp;gt;
    &amp;lt;% } %&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;!-- 동일한 기능의 Thymeleaf 예제 --&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;p th:if=&quot;${user}&quot; th:text=&quot;|Hello ${user.name}!|&quot;&amp;gt;Hello Guest!&amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;${user}&quot;&amp;gt;Please log in&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Thymeleaf vs Mustache&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- Mustache 예제 --&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
    {{#user}}
        &amp;lt;p&amp;gt;Hello {{name}}!&amp;lt;/p&amp;gt;
    {{/user}}
    {{^user}}
        &amp;lt;p&amp;gt;Please log in&amp;lt;/p&amp;gt;
    {{/user}}
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;!-- Thymeleaf 예제 (더 풍부한 표현식) --&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;p th:if=&quot;${user}&quot; 
       th:text=&quot;|Hello ${user.name?.toUpperCase() ?: 'Guest'}!|&quot;&amp;gt;
       Hello Guest!
    &amp;lt;/p&amp;gt;
    &amp;lt;p th:unless=&quot;${user}&quot;&amp;gt;Please log in&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mustache는 로직리스 템플릿으로 단순하지만 표현력이 제한적&lt;/li&gt;
&lt;li&gt;Thymeleaf는 메소드 호출, Elvis 연산자(&lt;code&gt;?:&lt;/code&gt;) 등 풍부한 표현식 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 Natural Templates의 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Natural Templates는 Thymeleaf의 핵심 철학입니다. 이는 템플릿 파일이 템플릿 엔진 없이도 웹 브라우저에서 의미 있게 표시될 수 있다는 개념입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;전통적인 방식의 문제점&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;!-- 전통적인 템플릿 (JSP, PHP 등) --&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;&amp;lt;?= $title ?&amp;gt;&amp;lt;/h1&amp;gt;
    &amp;lt;% for(User user : users) { %&amp;gt;
        &amp;lt;p&amp;gt;&amp;lt;%= user.getName() %&amp;gt; - &amp;lt;%= user.getEmail() %&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;% } %&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일을 브라우저에서 직접 열면 스크립트 코드가 그대로 표시되어 디자인 확인이 불가능합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;Thymeleaf의 Natural Templates 접근법&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title th:text=&quot;${pageTitle}&quot;&amp;gt;Default Page Title&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1 th:text=&quot;${title}&quot;&amp;gt;Sample Page Title&amp;lt;/h1&amp;gt;

    &amp;lt;!-- 사용자 목록 --&amp;gt;
    &amp;lt;div class=&quot;user-list&quot;&amp;gt;
        &amp;lt;div class=&quot;user-card&quot; th:each=&quot;user : ${users}&quot; th:remove=&quot;tag&quot;&amp;gt;
            &amp;lt;div class=&quot;user-info&quot;&amp;gt;
                &amp;lt;h3 th:text=&quot;${user.name}&quot;&amp;gt;John Doe&amp;lt;/h3&amp;gt;
                &amp;lt;p th:text=&quot;${user.email}&quot;&amp;gt;john.doe@example.com&amp;lt;/p&amp;gt;
                &amp;lt;span class=&quot;role&quot; th:text=&quot;${user.role}&quot;&amp;gt;admin&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;!-- 샘플 데이터 (실제 렌더링 시 제거됨) --&amp;gt;
        &amp;lt;div class=&quot;user-card&quot;&amp;gt;
            &amp;lt;div class=&quot;user-info&quot;&amp;gt;
                &amp;lt;h3&amp;gt;Jane Smith&amp;lt;/h3&amp;gt;
                &amp;lt;p&amp;gt;jane.smith@example.com&amp;lt;/p&amp;gt;
                &amp;lt;span class=&quot;role&quot;&amp;gt;user&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;user-card&quot;&amp;gt;
            &amp;lt;div class=&quot;user-info&quot;&amp;gt;
                &amp;lt;h3&amp;gt;Bob Johnson&amp;lt;/h3&amp;gt;
                &amp;lt;p&amp;gt;bob.johnson@example.com&amp;lt;/p&amp;gt;
                &amp;lt;span class=&quot;role&quot;&amp;gt;moderator&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Natural Templates의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;디자이너 친화적&lt;/b&gt;: HTML/CSS 디자이너가 실제 데이터 없이도 디자인을 확인할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프로토타이핑&lt;/b&gt;: 백엔드 개발 전에 프론트엔드 작업이 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;협업 향상&lt;/b&gt;: 디자이너와 개발자가 같은 파일로 작업 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수성&lt;/b&gt;: HTML 구조가 명확하게 보임&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.5 사용 사례와 적용 분야&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;웹 애플리케이션&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 전자상거래 상품 목록 --&amp;gt;
&amp;lt;div class=&quot;product-grid&quot;&amp;gt;
    &amp;lt;div class=&quot;product-card&quot; th:each=&quot;product : ${products}&quot;&amp;gt;
        &amp;lt;img th:src=&quot;@{/images/products/{id}.jpg(id=${product.id})}&quot; 
             th:alt=&quot;${product.name}&quot;
             src=&quot;/images/placeholder.jpg&quot; alt=&quot;Product Image&quot;&amp;gt;

        &amp;lt;h3 th:text=&quot;${product.name}&quot;&amp;gt;Sample Product&amp;lt;/h3&amp;gt;

        &amp;lt;div class=&quot;price&quot;&amp;gt;
            &amp;lt;span th:if=&quot;${product.discount &amp;gt; 0}&quot; class=&quot;original-price&quot; 
                  th:text=&quot;${#numbers.formatCurrency(product.originalPrice)}&quot;&amp;gt;$99.99&amp;lt;/span&amp;gt;
            &amp;lt;span class=&quot;current-price&quot; 
                  th:text=&quot;${#numbers.formatCurrency(product.currentPrice)}&quot;&amp;gt;$79.99&amp;lt;/span&amp;gt;
            &amp;lt;span th:if=&quot;${product.discount &amp;gt; 0}&quot; class=&quot;discount&quot; 
                  th:text=&quot;|${product.discount}% OFF|&quot;&amp;gt;20% OFF&amp;lt;/span&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;button th:onclick=&quot;|addToCart(${product.id})|&quot; 
                th:disabled=&quot;${!product.inStock}&quot;&amp;gt;
            &amp;lt;span th:text=&quot;${product.inStock} ? 'Add to Cart' : 'Out of Stock'&quot;&amp;gt;Add to Cart&amp;lt;/span&amp;gt;
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;이메일 템플릿&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 이메일 템플릿 --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title th:text=&quot;#{email.welcome.title}&quot;&amp;gt;Welcome Email&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body style=&quot;font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;&quot;&amp;gt;
    &amp;lt;div class=&quot;header&quot; style=&quot;background-color: #f8f9fa; padding: 20px;&quot;&amp;gt;
        &amp;lt;h1 th:text=&quot;#{email.welcome.header(${user.name})}&quot;&amp;gt;Welcome John!&amp;lt;/h1&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;content&quot; style=&quot;padding: 20px;&quot;&amp;gt;
        &amp;lt;p th:text=&quot;#{email.welcome.message}&quot;&amp;gt;Thank you for joining our service.&amp;lt;/p&amp;gt;

        &amp;lt;div class=&quot;account-info&quot;&amp;gt;
            &amp;lt;h3 th:text=&quot;#{email.account.details}&quot;&amp;gt;Account Details&amp;lt;/h3&amp;gt;
            &amp;lt;ul&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;strong th:text=&quot;#{email.username}&quot;&amp;gt;Username&amp;lt;/strong&amp;gt;: 
                    &amp;lt;span th:text=&quot;${user.username}&quot;&amp;gt;john_doe&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;strong th:text=&quot;#{email.email}&quot;&amp;gt;Email&amp;lt;/strong&amp;gt;: 
                    &amp;lt;span th:text=&quot;${user.email}&quot;&amp;gt;john@example.com&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;
                &amp;lt;li&amp;gt;&amp;lt;strong th:text=&quot;#{email.registered}&quot;&amp;gt;Registered&amp;lt;/strong&amp;gt;: 
                    &amp;lt;span th:text=&quot;${#temporals.format(user.createdAt, 'MMM dd, yyyy')}&quot;&amp;gt;Jan 15, 2024&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;
            &amp;lt;/ul&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;action&quot; style=&quot;text-align: center; margin: 30px 0;&quot;&amp;gt;
            &amp;lt;a th:href=&quot;@{https://oursite.com/activate/{token}(token=${activationToken})}&quot; 
               style=&quot;background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px;&quot;
               th:text=&quot;#{email.activate.button}&quot;&amp;gt;Activate Account&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;footer&quot; style=&quot;background-color: #f8f9fa; padding: 20px; text-align: center; font-size: 12px;&quot;&amp;gt;
        &amp;lt;p th:text=&quot;#{email.footer.thanks}&quot;&amp;gt;Thank you for choosing our service!&amp;lt;/p&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;a th:href=&quot;@{https://oursite.com}&quot; th:text=&quot;#{email.footer.website}&quot;&amp;gt;Visit Website&amp;lt;/a&amp;gt; |
            &amp;lt;a th:href=&quot;@{mailto:support@oursite.com}&quot; th:text=&quot;#{email.footer.support}&quot;&amp;gt;Contact Support&amp;lt;/a&amp;gt;
        &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;u&gt;관리자 대시보드&lt;/u&gt;&lt;/h4&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;!-- 관리자 대시보드 통계 --&amp;gt;
&amp;lt;div class=&quot;dashboard-stats&quot;&amp;gt;
    &amp;lt;div class=&quot;stat-card&quot; th:each=&quot;stat : ${dashboardStats}&quot;&amp;gt;
        &amp;lt;div class=&quot;stat-icon&quot; th:class=&quot;|icon-${stat.type}|&quot;&amp;gt;
            &amp;lt;i th:class=&quot;${stat.iconClass}&quot;&amp;gt;&amp;lt;/i&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;stat-content&quot;&amp;gt;
            &amp;lt;h3 th:text=&quot;${stat.title}&quot;&amp;gt;Total Users&amp;lt;/h3&amp;gt;
            &amp;lt;div class=&quot;stat-number&quot; th:text=&quot;${#numbers.formatInteger(stat.value, 0, 'COMMA')}&quot;&amp;gt;1,234&amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;stat-change&quot; th:classappend=&quot;${stat.changePercent &amp;gt;= 0} ? 'positive' : 'negative'&quot;&amp;gt;
                &amp;lt;span th:text=&quot;|${stat.changePercent &amp;gt;= 0 ? '+' : ''}${stat.changePercent}%|&quot;&amp;gt;+5.2%&amp;lt;/span&amp;gt;
                &amp;lt;span th:text=&quot;#{dashboard.from.lastmonth}&quot;&amp;gt;from last month&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- 최근 활동 로그 --&amp;gt;
&amp;lt;div class=&quot;recent-activity&quot;&amp;gt;
    &amp;lt;h2 th:text=&quot;#{dashboard.recent.activity}&quot;&amp;gt;Recent Activity&amp;lt;/h2&amp;gt;
    &amp;lt;div class=&quot;activity-list&quot;&amp;gt;
        &amp;lt;div class=&quot;activity-item&quot; th:each=&quot;activity : ${recentActivities}&quot;&amp;gt;
            &amp;lt;div class=&quot;activity-time&quot; th:text=&quot;${#temporals.format(activity.timestamp, 'HH:mm')}&quot;&amp;gt;14:30&amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;activity-content&quot;&amp;gt;
                &amp;lt;span th:text=&quot;${activity.description}&quot;&amp;gt;User logged in&amp;lt;/span&amp;gt;
                &amp;lt;small th:text=&quot;${activity.userEmail}&quot;&amp;gt;user@example.com&amp;lt;/small&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;activity-status&quot; th:class=&quot;|status-${activity.type}|&quot;&amp;gt;
                &amp;lt;span th:text=&quot;${activity.status}&quot;&amp;gt;Success&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설명:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;#numbers.formatInteger()&lt;/code&gt;: 숫자 포맷팅 (천 단위 콤마)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;th:classappend&lt;/code&gt;: 기존 클래스에 조건부 클래스 추가&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#temporals.format()&lt;/code&gt;: 날짜/시간 포맷팅&lt;/li&gt;
&lt;li&gt;복합적인 조건식과 표현식 활용으로 복잡한 UI 로직 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 다양한 사례들을 통해 Thymeleaf가 단순한 웹 페이지부터 복잡한 대시보드까지 다양한 용도로 활용될 수 있음을 알 수 있습니다.&lt;/p&gt;</description>
      <category>thymeleaf</category>
      <category>개요</category>
      <category>정의</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/139</guid>
      <comments>https://devsite.tistory.com/entry/Thymeleaf-%EA%B0%80%EC%9D%B4%EB%93%9C#entry139comment</comments>
      <pubDate>Mon, 25 Aug 2025 11:23:08 +0900</pubDate>
    </item>
    <item>
      <title>PostgreSQL 완전 가이드</title>
      <link>https://devsite.tistory.com/entry/PostgreSQL-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ya6z6/btsP4iwE6w8/6yLEzdBC1uQupfdjwPtD7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ya6z6/btsP4iwE6w8/6yLEzdBC1uQupfdjwPtD7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ya6z6/btsP4iwE6w8/6yLEzdBC1uQupfdjwPtD7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fya6z6%2FbtsP4iwE6w8%2F6yLEzdBC1uQupfdjwPtD7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;442&quot; height=&quot;203&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. PostgreSQL 소개 및 역사&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL의 탄생&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 1986년 캘리포니아 대학교 버클리에서 Michael Stonebraker 교수가 시작한 POSTGRES 프로젝트에서 출발했습니다. 1996년 SQL 지원이 추가되면서 PostgreSQL로 이름이 변경되었고, 현재는 세계에서 가장 고급 오픈소스 관계형 데이터베이스로 인정받고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 철학&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;확장성 (Extensibility)&lt;/b&gt;: 사용자가 새로운 데이터 타입, 함수, 연산자를 정의할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표준 준수&lt;/b&gt;: SQL 표준을 엄격히 따름&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: ACID 속성을 완벽히 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오픈소스&lt;/b&gt;: PostgreSQL License (BSD 스타일)로 자유롭게 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. PostgreSQL 고유 기능들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 MVCC (Multi-Version Concurrency Control)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL의 핵심 아키텍처 중 하나입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- MVCC 동작 원리 이해하기
-- 세션 1
BEGIN;
SELECT * FROM accounts WHERE id = 1; -- balance: 1000
UPDATE accounts SET balance = 900 WHERE id = 1;
-- 아직 COMMIT 안함

-- 세션 2 (동시에 실행)
SELECT * FROM accounts WHERE id = 1; -- 여전히 1000을 봄 (읽기 잠금 없음)

-- 세션 1
COMMIT; -- 이제 세션 2에서도 900을 보게 됨&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MVCC의 장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 작업이 쓰기 작업을 차단하지 않음&lt;/li&gt;
&lt;li&gt;높은 동시성 성능&lt;/li&gt;
&lt;li&gt;데드락 발생 가능성 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 고급 데이터 타입들&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. JSONB - Binary JSON (성능 최적화)
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT,
    specs JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO products (name, specs) VALUES 
('Laptop', '{&quot;brand&quot;: &quot;Apple&quot;, &quot;ram&quot;: &quot;16GB&quot;, &quot;storage&quot;: &quot;512GB&quot;, &quot;ports&quot;: [&quot;USB-C&quot;, &quot;Thunderbolt&quot;]}'),
('Phone', '{&quot;brand&quot;: &quot;Samsung&quot;, &quot;ram&quot;: &quot;8GB&quot;, &quot;storage&quot;: &quot;256GB&quot;, &quot;camera&quot;: {&quot;main&quot;: &quot;108MP&quot;, &quot;ultra&quot;: &quot;12MP&quot;}}');

-- JSONB 쿼리 예시
-- 1) 특정 키 값으로 검색
SELECT name FROM products WHERE specs-&amp;gt;&amp;gt;'brand' = 'Apple';

-- 2) 중첩 객체 접근
SELECT name FROM products WHERE specs-&amp;gt;'camera'-&amp;gt;&amp;gt;'main' = '108MP';

-- 3) 배열 요소 검색
SELECT name FROM products WHERE specs-&amp;gt;'ports' @&amp;gt; '[&quot;USB-C&quot;]';

-- 4) 키 존재 여부 확인
SELECT name FROM products WHERE specs ? 'camera';

-- 5) JSONB 함수들
SELECT 
    name,
    jsonb_object_keys(specs) as spec_keys,
    jsonb_typeof(specs-&amp;gt;'ram') as ram_type
FROM products;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 2. Array 타입
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title TEXT,
    tags TEXT[],
    ratings INTEGER[]
);

INSERT INTO articles (title, tags, ratings) VALUES 
('PostgreSQL Guide', ARRAY['database', 'postgresql', 'sql'], ARRAY[5, 4, 5, 3, 4]),
('Python Tutorial', ARRAY['programming', 'python'], ARRAY[4, 5, 4, 4]);

-- Array 쿼리 예시
-- 1) 특정 값 포함 검색
SELECT title FROM articles WHERE 'postgresql' = ANY(tags);

-- 2) 배열 연산
SELECT title, array_length(tags, 1) as tag_count FROM articles;

-- 3) 배열 요소 접근 (1부터 시작)
SELECT title, tags[1] as first_tag FROM articles;

-- 4) 배열 슬라이싱
SELECT title, tags[1:2] as first_two_tags FROM articles;

-- 5) 집계 함수와 함께
SELECT 
    title,
    ROUND(AVG(rating), 2) as avg_rating
FROM articles, unnest(ratings) as rating
GROUP BY title;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 3. Geometric 타입
CREATE TABLE locations (
    id SERIAL PRIMARY KEY,
    name TEXT,
    coordinates POINT,
    area POLYGON,
    route PATH
);

INSERT INTO locations (name, coordinates, area) VALUES 
('Seoul City Hall', POINT(126.978, 37.566), 
 POLYGON('((126.97, 37.56), (126.98, 37.56), (126.98, 37.57), (126.97, 37.57))'));

-- 거리 계산
SELECT 
    name,
    coordinates &amp;lt;-&amp;gt; POINT(126.980, 37.565) as distance
FROM locations
ORDER BY distance
LIMIT 5;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 4. Range 타입 (시간/숫자 범위)
CREATE TABLE reservations (
    id SERIAL PRIMARY KEY,
    room_id INT,
    time_range TSRANGE,
    price_range NUMRANGE
);

INSERT INTO reservations (room_id, time_range, price_range) VALUES 
(1, '[2024-08-09 09:00, 2024-08-09 17:00)', '[100000, 150000]'),
(2, '[2024-08-09 14:00, 2024-08-09 18:00)', '[80000, 120000]');

-- Range 쿼리
-- 1) 겹치는 예약 찾기
SELECT * FROM reservations 
WHERE time_range &amp;amp;&amp;amp; '[2024-08-09 15:00, 2024-08-09 16:00)'::TSRANGE;

-- 2) 특정 시간이 포함된 예약
SELECT * FROM reservations 
WHERE time_range @&amp;gt; '2024-08-09 15:30'::TIMESTAMP;

-- 3) Range 연산
SELECT 
    room_id,
    upper(time_range) - lower(time_range) as duration,
    upper(price_range) - lower(price_range) as price_difference
FROM reservations;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 전문 검색 (Full Text Search)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 기본 전문 검색 설정
CREATE TABLE documents (
    id SERIAL PRIMARY KEY,
    title TEXT,
    content TEXT,
    search_vector TSVECTOR
);

-- 검색 벡터 생성
UPDATE documents 
SET search_vector = to_tsvector('english', title || ' ' || content);

-- 인덱스 생성 (성능 향상)
CREATE INDEX idx_search_vector ON documents USING gin(search_vector);

-- 검색 예시
INSERT INTO documents (title, content) VALUES 
('PostgreSQL Advanced Features', 'PostgreSQL offers many advanced features like JSONB, arrays, and full text search'),
('Database Performance Tuning', 'Learn how to optimize your database queries for better performance');

UPDATE documents SET search_vector = to_tsvector('english', title || ' ' || content);

-- 1) 단순 검색
SELECT title FROM documents 
WHERE search_vector @@ to_tsquery('english', 'postgresql');

-- 2) 복합 검색 (AND, OR, NOT)
SELECT title FROM documents 
WHERE search_vector @@ to_tsquery('english', 'postgresql &amp;amp; advanced');

SELECT title FROM documents 
WHERE search_vector @@ to_tsquery('english', 'database | performance');

-- 3) 랭킹과 하이라이팅
SELECT 
    title,
    ts_rank(search_vector, query) as rank,
    ts_headline('english', content, query) as snippet
FROM documents, to_tsquery('english', 'postgresql') as query
WHERE search_vector @@ query
ORDER BY rank DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 사용자 정의 타입 및 도메인&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 복합 타입 정의
CREATE TYPE address AS (
    street TEXT,
    city TEXT,
    postal_code TEXT,
    country TEXT
);

CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    name TEXT,
    billing_address address,
    shipping_address address
);

INSERT INTO customers (name, billing_address, shipping_address) VALUES 
('김철수', ROW('강남대로 123', '서울', '06292', '한국')::address,
         ROW('판교로 456', '성남', '13494', '한국')::address);

-- 복합 타입 쿼리
SELECT 
    name,
    (billing_address).city as billing_city,
    (shipping_address).city as shipping_city
FROM customers;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 2. 도메인 (제약조건이 있는 타입)
CREATE DOMAIN email AS TEXT
CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');

CREATE DOMAIN positive_numeric AS NUMERIC
CHECK (VALUE &amp;gt; 0);

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username TEXT UNIQUE NOT NULL,
    email_address email,
    balance positive_numeric DEFAULT 0
);

-- 도메인 제약 조건 테스트
INSERT INTO users (username, email_address, balance) VALUES 
('john_doe', 'john@example.com', 1000.50);

-- 이건 실패함: CHECK 제약 위반
-- INSERT INTO users (username, email_address, balance) VALUES 
-- ('jane', 'invalid-email', -100);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 고급 쿼리 기능들&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Window Functions (윈도우 함수)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 샘플 데이터
CREATE TABLE sales (
    id SERIAL PRIMARY KEY,
    employee_id INT,
    department TEXT,
    sale_date DATE,
    amount DECIMAL(10,2)
);

INSERT INTO sales (employee_id, department, sale_date, amount) VALUES 
(1, 'IT', '2024-01-15', 5000),
(2, 'IT', '2024-01-20', 3000),
(3, 'Sales', '2024-01-10', 8000),
(1, 'IT', '2024-02-15', 5500),
(4, 'Sales', '2024-02-20', 7200),
(2, 'IT', '2024-02-25', 3200);

-- 1) 순위 함수들
SELECT 
    employee_id,
    department,
    amount,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY amount DESC) as row_num,
    RANK() OVER (PARTITION BY department ORDER BY amount DESC) as rank,
    DENSE_RANK() OVER (PARTITION BY department ORDER BY amount DESC) as dense_rank,
    PERCENT_RANK() OVER (PARTITION BY department ORDER BY amount DESC) as percent_rank
FROM sales;

-- 2) 집계 윈도우 함수
SELECT 
    employee_id,
    sale_date,
    amount,
    SUM(amount) OVER (PARTITION BY employee_id ORDER BY sale_date) as running_total,
    AVG(amount) OVER (PARTITION BY employee_id ORDER BY sale_date 
                      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) as moving_avg_3,
    COUNT(*) OVER (PARTITION BY employee_id) as total_sales_count
FROM sales;

-- 3) LAG/LEAD 함수
SELECT 
    employee_id,
    sale_date,
    amount,
    LAG(amount, 1) OVER (PARTITION BY employee_id ORDER BY sale_date) as prev_sale,
    LEAD(amount, 1) OVER (PARTITION BY employee_id ORDER BY sale_date) as next_sale,
    amount - LAG(amount, 1) OVER (PARTITION BY employee_id ORDER BY sale_date) as growth
FROM sales;

-- 4) NTILE (분위수)
SELECT 
    employee_id,
    amount,
    NTILE(4) OVER (ORDER BY amount) as quartile,
    CASE NTILE(4) OVER (ORDER BY amount)
        WHEN 1 THEN 'Low Performer'
        WHEN 2 THEN 'Average'
        WHEN 3 THEN 'Good'
        WHEN 4 THEN 'Top Performer'
    END as performance_category
FROM sales;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Common Table Expressions (CTE)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 재귀 CTE - 조직도 구성
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name TEXT,
    manager_id INT REFERENCES employees(id),
    department TEXT,
    salary DECIMAL(10,2)
);

INSERT INTO employees VALUES 
(1, 'CEO Kim', NULL, 'Executive', 10000000),
(2, 'CTO Lee', 1, 'Technology', 8000000),
(3, 'CFO Park', 1, 'Finance', 8000000),
(4, 'Dev Manager Choi', 2, 'Technology', 6000000),
(5, 'Senior Dev Jung', 4, 'Technology', 5000000),
(6, 'Junior Dev Yoon', 4, 'Technology', 3500000),
(7, 'Accountant Lim', 3, 'Finance', 4000000);

-- 재귀 CTE로 조직 계층구조 탐색
WITH RECURSIVE org_chart AS (
    -- Base case: 최고 관리자들
    SELECT id, name, manager_id, department, salary, 0 as level, 
           name as path, ARRAY[id] as id_path
    FROM employees 
    WHERE manager_id IS NULL

    UNION ALL

    -- Recursive case: 하위 직원들
    SELECT e.id, e.name, e.manager_id, e.department, e.salary, 
           oc.level + 1,
           oc.path || ' -&amp;gt; ' || e.name,
           oc.id_path || e.id
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT 
    REPEAT('  ', level) || name as hierarchy,
    department,
    salary,
    path
FROM org_chart
ORDER BY id_path;

-- 2. 비재귀 CTE - 복잡한 분석
WITH monthly_sales AS (
    SELECT 
        DATE_TRUNC('month', sale_date) as month,
        department,
        SUM(amount) as total_amount,
        COUNT(*) as sale_count,
        AVG(amount) as avg_amount
    FROM sales 
    GROUP BY 1, 2
),
department_stats AS (
    SELECT 
        department,
        AVG(total_amount) as avg_monthly_sales,
        STDDEV(total_amount) as stddev_monthly_sales
    FROM monthly_sales
    GROUP BY department
)
SELECT 
    ms.month,
    ms.department,
    ms.total_amount,
    ds.avg_monthly_sales,
    CASE 
        WHEN ms.total_amount &amp;gt; ds.avg_monthly_sales + ds.stddev_monthly_sales 
        THEN 'Above Average'
        WHEN ms.total_amount &amp;lt; ds.avg_monthly_sales - ds.stddev_monthly_sales 
        THEN 'Below Average'
        ELSE 'Normal'
    END as performance
FROM monthly_sales ms
JOIN department_stats ds ON ms.department = ds.department
ORDER BY ms.month, ms.department;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 고급 집계 함수들&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. GROUPING SETS, ROLLUP, CUBE
SELECT 
    department,
    DATE_TRUNC('month', sale_date) as month,
    COUNT(*) as sale_count,
    SUM(amount) as total_amount
FROM sales
GROUP BY GROUPING SETS (
    (department),                    -- 부서별 총계
    (DATE_TRUNC('month', sale_date)), -- 월별 총계
    (department, DATE_TRUNC('month', sale_date)), -- 부서+월별
    ()                              -- 전체 총계
)
ORDER BY department, month;

-- ROLLUP 사용
SELECT 
    department,
    employee_id,
    COUNT(*) as sale_count,
    SUM(amount) as total_amount
FROM sales
GROUP BY ROLLUP(department, employee_id)
ORDER BY department, employee_id;

-- 2. 통계 함수들
SELECT 
    department,
    COUNT(*) as count,
    AVG(amount) as mean,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY amount) as median,
    MODE() WITHIN GROUP (ORDER BY amount) as mode,
    STDDEV_POP(amount) as std_dev,
    VAR_POP(amount) as variance
FROM sales
GROUP BY department;

-- 3. 배열 집계 함수
SELECT 
    department,
    ARRAY_AGG(amount ORDER BY amount DESC) as amounts_desc,
    STRING_AGG(employee_id::TEXT, ', ' ORDER BY amount DESC) as top_employees
FROM sales
GROUP BY department;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 인덱스 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 인덱스 타입들&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. B-tree 인덱스 (기본)
CREATE INDEX idx_sales_date ON sales(sale_date);
CREATE INDEX idx_sales_dept_amount ON sales(department, amount);

-- 2. 부분 인덱스 (조건부)
CREATE INDEX idx_high_value_sales ON sales(sale_date) 
WHERE amount &amp;gt; 5000;

-- 3. 표현식 인덱스
CREATE INDEX idx_sales_month ON sales(DATE_TRUNC('month', sale_date));
CREATE INDEX idx_employee_upper_name ON employees(UPPER(name));

-- 4. GIN 인덱스 (배열, JSONB, 전문검색용)
CREATE INDEX idx_products_specs ON products USING gin(specs);
CREATE INDEX idx_articles_tags ON articles USING gin(tags);

-- 5. GiST 인덱스 (기하학적 데이터, 범위 타입)
CREATE INDEX idx_locations_coordinates ON locations USING gist(coordinates);
CREATE INDEX idx_reservations_time ON reservations USING gist(time_range);

-- 6. Hash 인덱스 (등등 비교만)
CREATE INDEX idx_products_id_hash ON products USING hash(id);

-- 7. BRIN 인덱스 (대용량 순차 데이터)
CREATE INDEX idx_sales_date_brin ON sales USING brin(sale_date);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 인덱스 사용 분석&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 실행 계획 분석
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) 
SELECT * FROM sales 
WHERE department = 'IT' AND amount &amp;gt; 3000;

-- 인덱스 사용 통계 확인
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

-- 사용되지 않는 인덱스 찾기
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname = 'public';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 쿼리 최적화&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. EXPLAIN 활용
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)
SELECT s.*, e.name
FROM sales s
JOIN employees e ON s.employee_id = e.id
WHERE s.sale_date &amp;gt;= '2024-01-01' AND s.amount &amp;gt; 4000;

-- 2. 통계 업데이트
ANALYZE sales;
ANALYZE employees;

-- 자동 통계 수집 확인
SELECT 
    schemaname,
    tablename,
    last_analyze,
    last_autoanalyze,
    n_tup_ins,
    n_tup_upd,
    n_tup_del
FROM pg_stat_user_tables;

-- 3. 쿼리 최적화 예시
-- 비효율적인 쿼리
SELECT * FROM sales s
WHERE EXISTS (
    SELECT 1 FROM employees e 
    WHERE e.id = s.employee_id AND e.department = 'IT'
);

-- 최적화된 쿼리
SELECT s.* FROM sales s
INNER JOIN employees e ON s.employee_id = e.id
WHERE e.department = 'IT';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 파티셔닝&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. Range 파티셔닝 (날짜별)
CREATE TABLE sales_partitioned (
    id SERIAL,
    employee_id INT,
    department TEXT,
    sale_date DATE,
    amount DECIMAL(10,2)
) PARTITION BY RANGE (sale_date);

-- 파티션 생성
CREATE TABLE sales_2024_q1 PARTITION OF sales_partitioned
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE TABLE sales_2024_q2 PARTITION OF sales_partitioned
    FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

-- 2. Hash 파티셔닝 (고르게 분산)
CREATE TABLE customer_data (
    id SERIAL,
    customer_id INT,
    data JSONB
) PARTITION BY HASH (customer_id);

CREATE TABLE customer_data_0 PARTITION OF customer_data
    FOR VALUES WITH (modulus 4, remainder 0);

CREATE TABLE customer_data_1 PARTITION OF customer_data
    FOR VALUES WITH (modulus 4, remainder 1);

-- 3. List 파티셔닝 (특정 값들)
CREATE TABLE sales_by_region (
    id SERIAL,
    region TEXT,
    amount DECIMAL
) PARTITION BY LIST (region);

CREATE TABLE sales_asia PARTITION OF sales_by_region
    FOR VALUES IN ('Korea', 'Japan', 'China');

CREATE TABLE sales_europe PARTITION OF sales_by_region
    FOR VALUES IN ('Germany', 'France', 'UK');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 연결 풀링 및 성능 모니터링&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 현재 연결 상태 확인
SELECT 
    pid,
    usename,
    application_name,
    client_addr,
    state,
    query_start,
    state_change,
    query
FROM pg_stat_activity
WHERE state = 'active';

-- 2. 느린 쿼리 찾기
SELECT 
    query,
    calls,
    total_time,
    rows,
    100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;

-- 3. 테이블 사용 통계
SELECT 
    schemaname,
    tablename,
    seq_scan,
    seq_tup_read,
    idx_scan,
    idx_tup_fetch,
    n_tup_ins,
    n_tup_upd,
    n_tup_del
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;

-- 4. 인덱스 효율성 분석
SELECT 
    t.tablename,
    indexname,
    c.reltuples AS num_rows,
    pg_size_pretty(pg_relation_size(quote_ident(t.schemaname)||'.'||quote_ident(t.tablename))) AS table_size,
    pg_size_pretty(pg_relation_size(quote_ident(t.schemaname)||'.'||quote_ident(t.indexrelname))) AS index_size,
    CASE WHEN indisunique THEN 'Y' ELSE 'N' END AS unique,
    idx_scan AS number_of_scans,
    idx_tup_read AS tuples_read,
    idx_tup_fetch AS tuples_fetched
FROM pg_tables t
LEFT JOIN pg_class c ON c.relname=t.tablename
LEFT JOIN pg_indexes i ON i.tablename=t.tablename
LEFT JOIN pg_stat_user_indexes ui ON ui.indexrelname=i.indexname
LEFT JOIN pg_index ix ON ix.indexrelid=ui.indexrelid
WHERE t.schemaname='public'
ORDER BY pg_relation_size(quote_ident(t.schemaname)||'.'||quote_ident(t.indexrelname)) DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 확장(Extension) 생태계&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 주요 확장들&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. PostGIS (지리 공간 데이터)
CREATE EXTENSION postgis;

CREATE TABLE restaurants (
    id SERIAL PRIMARY KEY,
    name TEXT,
    location GEOGRAPHY(POINT, 4326)
);

INSERT INTO restaurants (name, location) VALUES 
('Pizza House', ST_GeogFromText('POINT(126.9780 37.5665)')),
('Burger King', ST_GeogFromText('POINT(126.9850 37.5700)'));

-- 거리 기반 검색
SELECT name, ST_Distance(location, ST_GeogFromText('POINT(126.9800 37.5680)')) as distance
FROM restaurants
WHERE ST_DWithin(location, ST_GeogFromText('POINT(126.9800 37.5680)'), 1000)
ORDER BY distance;

-- 2. pg_stat_statements (쿼리 성능 분석)
CREATE EXTENSION pg_stat_statements;

-- 3. uuid-ossp (UUID 생성)
CREATE EXTENSION &quot;uuid-ossp&quot;;

CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    customer_id INT,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 4. ltree (계층 구조 데이터)
CREATE EXTENSION ltree;

CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    path LTREE,
    name TEXT
);

INSERT INTO categories (path, name) VALUES 
('electronics', 'Electronics'),
('electronics.computers', 'Computers'),
('electronics.computers.laptops', 'Laptops'),
('electronics.phones', 'Phones'),
('books', 'Books'),
('books.fiction', 'Fiction');

-- 계층 구조 쿼리
SELECT * FROM categories WHERE path &amp;lt;@ 'electronics';
SELECT * FROM categories WHERE path ~ 'electronics.*';

-- 5. pg_trgm (유사도 검색)
CREATE EXTENSION pg_trgm;

CREATE INDEX idx_products_name_trgm ON products USING gin(name gin_trgm_ops);

-- 퍼지 검색
SELECT name, similarity(name, 'laptop') as sim
FROM products
WHERE similarity(name, 'laptop') &amp;gt; 0.3
ORDER BY sim DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 커스텀 함수 및 프로시저&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. PL/pgSQL 함수
CREATE OR REPLACE FUNCTION calculate_bonus(emp_id INT)
RETURNS DECIMAL(10,2) AS $$
DECLARE
    emp_salary DECIMAL(10,2);
    bonus_rate DECIMAL(3,2);
    total_bonus DECIMAL(10,2);
BEGIN
    -- 직원 급여 조회
    SELECT salary INTO emp_salary 
    FROM employees 
    WHERE id = emp_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Employee not found: %', emp_id;
    END IF;

    -- 급여에 따른 보너스율 결정
    CASE 
        WHEN emp_salary &amp;gt;= 8000000 THEN bonus_rate := 0.20;
        WHEN emp_salary &amp;gt;= 5000000 THEN bonus_rate := 0.15;
        WHEN emp_salary &amp;gt;= 3000000 THEN bonus_rate := 0.10;
        ELSE bonus_rate := 0.05;
    END CASE;

    total_bonus := emp_salary * bonus_rate;

    RETURN total_bonus;
END;
$$ LANGUAGE plpgsql;

-- 함수 사용
SELECT name, salary, calculate_bonus(id) as bonus
FROM employees;

-- 2. 트리거 함수
CREATE OR REPLACE FUNCTION update_employee_count()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        UPDATE departments 
        SET employee_count = employee_count + 1 
        WHERE name = NEW.department;
        RETURN NEW;
    ELSIF TG_OP = 'DELETE' THEN
        UPDATE departments 
        SET employee_count = employee_count - 1 
        WHERE name = OLD.department;
        RETURN OLD;
    END IF;
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_employee_count
    AFTER INSERT OR DELETE ON employees
    FOR EACH ROW EXECUTE FUNCTION update_employee_count();

-- 3. 집계 함수 생성
CREATE OR REPLACE FUNCTION median_finalfunc(anyarray)
RETURNS ANYELEMENT AS $$
SELECT CASE
    WHEN array_length($1, 1) % 2 = 1 THEN
        $1[(array_length($1, 1) + 1) / 2]
    ELSE
        ($1[array_length($1, 1) / 2] + $1[array_length($1, 1) / 2 + 1]) / 2
END;
$$ LANGUAGE SQL IMMUTABLE;

CREATE AGGREGATE median(ANYELEMENT) (
    SFUNC = array_append,
    STYPE = anyarray,
    FINALFUNC = median_finalfunc,
    INITCOND = '{}'
);

-- 사용 예시
SELECT department, median(salary) as median_salary
FROM employees
GROUP BY department;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 백업 및 복구 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.1 논리적 백업 (pg_dump/pg_restore)&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 1. 전체 데이터베이스 백업
pg_dump -U postgres -h localhost -W -F t -b -v -f mydb_backup.tar mydb

# 2. 특정 테이블만 백업
pg_dump -U postgres -h localhost -W -t employees -t sales -f specific_tables.sql mydb

# 3. 스키마만 백업 (데이터 제외)
pg_dump -U postgres -h localhost -W -s -f schema_only.sql mydb

# 4. 데이터만 백업 (스키마 제외)
pg_dump -U postgres -h localhost -W -a -f data_only.sql mydb

# 5. 압축 백업
pg_dump -U postgres -h localhost -W -F c -Z 9 -f compressed_backup.backup mydb&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# 복구 예시
# 1. tar 형식 복구
pg_restore -U postgres -h localhost -W -d restored_db -v mydb_backup.tar

# 2. 선택적 복구 (특정 테이블만)
pg_restore -U postgres -h localhost -W -d mydb -t employees mydb_backup.tar

# 3. 스키마만 복구
pg_restore -U postgres -h localhost -W -d mydb -s mydb_backup.tar

# 4. 병렬 복구 (성능 향상)
pg_restore -U postgres -h localhost -W -d mydb -j 4 -v mydb_backup.tar&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.2 물리적 백업 (Point-in-Time Recovery)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. WAL 아카이빙 설정 확인
SHOW wal_level;        -- replica 이상이어야 함
SHOW archive_mode;     -- on이어야 함
SHOW archive_command;  -- 아카이브 명령 확인

-- 2. 베이스 백업 생성
-- postgresql.conf 설정
-- archive_mode = on
-- archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'
-- wal_level = replica&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# 베이스 백업 실행
pg_basebackup -U postgres -h localhost -W -D /backup/base_backup -P -v -W

# Point-in-Time Recovery 복구 예시
# 1. 서버 중지
systemctl stop postgresql

# 2. 데이터 디렉토리 백업
mv /var/lib/postgresql/data /var/lib/postgresql/data_old

# 3. 베이스 백업 복원
cp -r /backup/base_backup /var/lib/postgresql/data

# 4. recovery.conf 생성 (PostgreSQL 12 이전)
# 또는 postgresql.conf에 복구 설정 추가 (PostgreSQL 12 이후)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.3 연속 아카이빙 및 스트리밍 복제&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 마스터 서버 설정
-- postgresql.conf
-- listen_addresses = '*'
-- wal_level = replica
-- max_wal_senders = 3
-- max_replication_slots = 3
-- synchronous_commit = on

-- pg_hba.conf에 복제 권한 추가
-- host replication replicator slave_ip/32 md5

-- 복제 사용자 생성
CREATE USER replicator REPLICATION LOGIN ENCRYPTED PASSWORD 'password';

-- 복제 슬롯 생성 (옵션)
SELECT pg_create_physical_replication_slot('slave1_slot');&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;# 슬레이브 서버 설정
# 1. 베이스 백업으로 초기 동기화
pg_basebackup -h master_ip -D /var/lib/postgresql/data -U replicator -P -v -W

# 2. recovery.conf 또는 postgresql.conf 설정
# standby_mode = 'on'
# primary_conninfo = 'host=master_ip port=5432 user=replicator password=password'
# primary_slot_name = 'slave1_slot'&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.4 백업 검증 및 자동화&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 백업 검증 스크립트
DO $
DECLARE
    backup_size BIGINT;
    table_count INT;
    expected_tables INT := 10; -- 예상 테이블 수
BEGIN
    -- 테이블 수 확인
    SELECT COUNT(*) INTO table_count 
    FROM information_schema.tables 
    WHERE table_schema = 'public';

    IF table_count &amp;lt; expected_tables THEN
        RAISE EXCEPTION 'Backup verification failed: Expected % tables, found %', 
            expected_tables, table_count;
    END IF;

    -- 데이터 일관성 검사 예시
    PERFORM 1 FROM employees WHERE salary &amp;lt; 0;
    IF FOUND THEN
        RAISE EXCEPTION 'Data consistency check failed: Invalid salary values found';
    END IF;

    RAISE NOTICE 'Backup verification passed: % tables found', table_count;
END $;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 보안 및 권한 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.1 사용자 및 역할 관리&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 역할 생성 및 관리
CREATE ROLE developer_role;
CREATE ROLE analyst_role;
CREATE ROLE admin_role;

-- 2. 권한 부여
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO developer_role;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst_role;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin_role;

-- 3. 사용자 생성 및 역할 할당
CREATE USER john_doe LOGIN PASSWORD 'secure_password';
CREATE USER jane_analyst LOGIN PASSWORD 'another_password';
CREATE USER admin_user LOGIN PASSWORD 'admin_password' CREATEDB CREATEROLE;

GRANT developer_role TO john_doe;
GRANT analyst_role TO jane_analyst;
GRANT admin_role TO admin_user;

-- 4. 세밀한 권한 제어
-- 특정 컬럼만 접근 허용
GRANT SELECT (id, name, department) ON employees TO analyst_role;

-- 특정 행만 접근 허용 (Row Level Security)
ALTER TABLE employees ENABLE ROW LEVEL SECURITY;

CREATE POLICY employee_policy ON employees
    FOR ALL TO developer_role
    USING (department = current_setting('app.current_department'));

-- 사용 시 세션 변수 설정
SET app.current_department = 'IT';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8.2 데이터 암호화 및 보안&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. pgcrypto 확장 사용
CREATE EXTENSION pgcrypto;

-- 비밀번호 해시화
CREATE TABLE secure_users (
    id SERIAL PRIMARY KEY,
    username TEXT UNIQUE,
    password_hash TEXT,
    email TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 비밀번호 저장 시 해시화
INSERT INTO secure_users (username, password_hash, email) VALUES 
('testuser', crypt('mypassword', gen_salt('bf', 8)), 'test@example.com');

-- 로그인 검증
SELECT id, username 
FROM secure_users 
WHERE username = 'testuser' 
  AND password_hash = crypt('mypassword', password_hash);

-- 2. 민감한 데이터 암호화
ALTER TABLE employees ADD COLUMN ssn_encrypted BYTEA;

-- 데이터 암호화 저장
UPDATE employees 
SET ssn_encrypted = pgp_sym_encrypt('123-45-6789', 'encryption_key')
WHERE id = 1;

-- 데이터 복호화 조회
SELECT id, name, 
       pgp_sym_decrypt(ssn_encrypted, 'encryption_key') as ssn
FROM employees 
WHERE id = 1;

-- 3. 감사 로그 테이블
CREATE TABLE audit_log (
    id SERIAL PRIMARY KEY,
    table_name TEXT,
    operation TEXT,
    old_values JSONB,
    new_values JSONB,
    user_name TEXT DEFAULT current_user,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 감사 트리거 함수
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $
BEGIN
    IF TG_OP = 'DELETE' THEN
        INSERT INTO audit_log (table_name, operation, old_values)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD));
        RETURN OLD;
    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO audit_log (table_name, operation, old_values, new_values)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(OLD), row_to_json(NEW));
        RETURN NEW;
    ELSIF TG_OP = 'INSERT' THEN
        INSERT INTO audit_log (table_name, operation, new_values)
        VALUES (TG_TABLE_NAME, TG_OP, row_to_json(NEW));
        RETURN NEW;
    END IF;
    RETURN NULL;
END;
$ LANGUAGE plpgsql;

-- 감사 트리거 적용
CREATE TRIGGER audit_employees_trigger
    AFTER INSERT OR UPDATE OR DELETE ON employees
    FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;9. 고가용성 및 확장성&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.1 연결 풀링&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Python 예시 - pgbouncer 설정과 연동
import psycopg2
from psycopg2 import pool

# 연결 풀 생성
connection_pool = psycopg2.pool.ThreadedConnectionPool(
    minconn=1,
    maxconn=20,
    host='localhost',
    database='mydb',
    user='postgres',
    password='password',
    port=6432  # pgbouncer 포트
)

def get_db_connection():
    return connection_pool.getconn()

def return_db_connection(conn):
    connection_pool.putconn(conn)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.2 읽기 복제본 활용&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 읽기 전용 복제본에서 실행할 쿼리 분리
-- 애플리케이션에서 읽기/쓰기 분리 전략

-- 마스터에서만 실행 (쓰기 작업)
INSERT INTO sales (employee_id, department, sale_date, amount) 
VALUES (1, 'IT', CURRENT_DATE, 5000);

-- 읽기 복제본에서 실행 가능 (읽기 작업)
SELECT 
    department,
    COUNT(*) as sale_count,
    SUM(amount) as total_amount
FROM sales 
WHERE sale_date &amp;gt;= CURRENT_DATE - INTERVAL '30 days'
GROUP BY department;

-- 복제 지연 모니터링
SELECT 
    application_name,
    client_addr,
    state,
    sync_state,
    pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) as pending_bytes,
    pg_wal_lsn_diff(sent_lsn, flush_lsn) as flush_lag_bytes
FROM pg_stat_replication;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9.3 파티셔닝 고급 활용&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 시간 기반 자동 파티션 생성 (pg_partman 확장 사용)
CREATE EXTENSION pg_partman;

-- 월별 파티션 테이블
CREATE TABLE sales_monthly (
    id BIGSERIAL,
    sale_date DATE NOT NULL,
    amount DECIMAL(10,2),
    employee_id INT
) PARTITION BY RANGE (sale_date);

-- pg_partman으로 자동 파티션 관리 설정
SELECT partman.create_parent(
    p_parent_table =&amp;gt; 'public.sales_monthly',
    p_control =&amp;gt; 'sale_date',
    p_type =&amp;gt; 'range',
    p_interval =&amp;gt; 'monthly',
    p_premake =&amp;gt; 2  -- 미리 생성할 파티션 수
);

-- 파티션 프루닝 확인
EXPLAIN (ANALYZE, BUFFERS) 
SELECT * FROM sales_monthly 
WHERE sale_date BETWEEN '2024-01-01' AND '2024-01-31';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;10. 모니터링 및 튜닝&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.1 성능 모니터링 쿼리들&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 활성 쿼리 및 잠금 상태
SELECT 
    pid,
    now() - pg_stat_activity.query_start AS duration,
    query,
    state,
    wait_event,
    wait_event_type
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) &amp;gt; interval '5 minutes'
  AND state = 'active';

-- 2. 잠금 대기 상황 분석
SELECT 
    blocked_locks.pid AS blocked_pid,
    blocked_activity.usename AS blocked_user,
    blocking_locks.pid AS blocking_pid,
    blocking_activity.usename AS blocking_user,
    blocked_activity.query AS blocked_statement,
    blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks 
    ON blocking_locks.locktype = blocked_locks.locktype
    AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
    AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
    AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
    AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
    AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
    AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
    AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
    AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
    AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
    AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;

-- 3. 테이블 크기 및 사용 통계
SELECT 
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
    pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) as table_size,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename) - pg_relation_size(schemaname||'.'||tablename)) as index_size,
    n_tup_ins,
    n_tup_upd,
    n_tup_del,
    n_live_tup,
    n_dead_tup,
    last_vacuum,
    last_autovacuum,
    last_analyze,
    last_autoanalyze
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;

-- 4. 캐시 적중률 분석
SELECT 
    'Database' as type,
    datname as name,
    numbackends as connections,
    round(100.0 * blks_hit / (blks_hit + blks_read), 2) as cache_hit_ratio
FROM pg_stat_database
WHERE datname = current_database()

UNION ALL

SELECT 
    'Table' as type,
    schemaname || '.' || tablename as name,
    NULL as connections,
    round(100.0 * heap_blks_hit / nullif(heap_blks_hit + heap_blks_read, 0), 2) as cache_hit_ratio
FROM pg_statio_user_tables
WHERE heap_blks_read &amp;gt; 0
ORDER BY cache_hit_ratio;

-- 5. 자주 실행되는 쿼리 분석 (pg_stat_statements 필요)
SELECT 
    query,
    calls,
    total_time,
    round(total_time / calls, 2) as avg_time,
    rows,
    round(100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0), 2) as hit_percent
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10.2 자동 튜닝 및 유지보수&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. VACUUM 및 ANALYZE 자동화 설정 확인
SELECT 
    name,
    setting,
    unit,
    short_desc
FROM pg_settings 
WHERE name LIKE '%autovacuum%' OR name LIKE '%vacuum%'
ORDER BY name;

-- 2. 테이블별 VACUUM 설정 커스터마이징
ALTER TABLE high_activity_table SET (
    autovacuum_vacuum_scale_factor = 0.1,  -- 기본값 0.2보다 더 자주
    autovacuum_analyze_scale_factor = 0.05,
    autovacuum_vacuum_cost_delay = 10
);

-- 3. 통계 수집 목표 조정
ALTER TABLE important_table ALTER COLUMN search_column SET STATISTICS 1000;
-- 기본값은 100, 높일수록 더 정확한 통계 수집

-- 4. 데이터베이스 유지보수 스크립트
DO $
DECLARE
    r RECORD;
    table_size BIGINT;
    dead_tuple_ratio FLOAT;
BEGIN
    FOR r IN SELECT schemaname, tablename FROM pg_stat_user_tables LOOP
        -- 테이블 크기 및 dead tuple 비율 확인
        SELECT 
            pg_relation_size(r.schemaname||'.'||r.tablename),
            CASE 
                WHEN n_live_tup + n_dead_tup &amp;gt; 0 
                THEN n_dead_tup::FLOAT / (n_live_tup + n_dead_tup) 
                ELSE 0 
            END
        INTO table_size, dead_tuple_ratio
        FROM pg_stat_user_tables 
        WHERE schemaname = r.schemaname AND tablename = r.tablename;

        -- 큰 테이블이고 dead tuple이 많으면 수동 VACUUM
        IF table_size &amp;gt; 100 * 1024 * 1024 AND dead_tuple_ratio &amp;gt; 0.1 THEN  -- 100MB 이상, 10% 이상
            RAISE NOTICE 'Running VACUUM on %.%', r.schemaname, r.tablename;
            EXECUTE format('VACUUM ANALYZE %I.%I', r.schemaname, r.tablename);
        END IF;
    END LOOP;
END $;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;11. 실제 사용 사례 및 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.1 시계열 데이터 처리&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- TimescaleDB 스타일의 시계열 데이터 처리 (네이티브 PostgreSQL)
CREATE TABLE sensor_data (
    time TIMESTAMPTZ NOT NULL,
    sensor_id INTEGER NOT NULL,
    temperature DECIMAL(5,2),
    humidity DECIMAL(5,2),
    pressure DECIMAL(7,2)
);

-- 하이퍼테이블 스타일 파티셔닝 (시간 기반)
CREATE TABLE sensor_data_2024_01 PARTITION OF sensor_data
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

-- 시계열 집계 쿼리
SELECT 
    time_bucket('1 hour', time) as hour,
    sensor_id,
    AVG(temperature) as avg_temp,
    MAX(temperature) as max_temp,
    MIN(temperature) as min_temp,
    STDDEV(temperature) as temp_stddev
FROM sensor_data
WHERE time &amp;gt;= NOW() - INTERVAL '7 days'
GROUP BY 1, 2
ORDER BY 1, 2;

-- 시간 버킷 함수 정의
CREATE OR REPLACE FUNCTION time_bucket(bucket_width INTERVAL, ts TIMESTAMPTZ)
RETURNS TIMESTAMPTZ AS $
    SELECT TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM ts) / EXTRACT(EPOCH FROM bucket_width)) * EXTRACT(EPOCH FROM bucket_width))::TIMESTAMPTZ;
$ LANGUAGE SQL IMMUTABLE;

-- 이상 값 탐지 (Z-score 기반)
WITH stats AS (
    SELECT 
        sensor_id,
        AVG(temperature) as avg_temp,
        STDDEV(temperature) as stddev_temp
    FROM sensor_data
    WHERE time &amp;gt;= NOW() - INTERVAL '24 hours'
    GROUP BY sensor_id
)
SELECT 
    sd.time,
    sd.sensor_id,
    sd.temperature,
    ABS(sd.temperature - s.avg_temp) / s.stddev_temp as z_score
FROM sensor_data sd
JOIN stats s ON sd.sensor_id = s.sensor_id
WHERE ABS(sd.temperature - s.avg_temp) / s.stddev_temp &amp;gt; 2  -- 2 표준편차 초과
  AND sd.time &amp;gt;= NOW() - INTERVAL '1 hour'
ORDER BY z_score DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.2 전자상거래 패턴&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 장바구니 및 주문 시스템
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    inventory_count INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE carts (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    customer_id INTEGER REFERENCES customers(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE cart_items (
    cart_id UUID REFERENCES carts(id) ON DELETE CASCADE,
    product_id INTEGER REFERENCES products(id),
    quantity INTEGER NOT NULL CHECK (quantity &amp;gt; 0),
    added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (cart_id, product_id)
);

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(id),
    status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')),
    total_amount DECIMAL(10,2) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE order_items (
    order_id INTEGER REFERENCES orders(id),
    product_id INTEGER REFERENCES products(id),
    quantity INTEGER NOT NULL,
    unit_price DECIMAL(10,2) NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

-- 장바구니에서 주문으로 변환하는 저장 프로시저
CREATE OR REPLACE FUNCTION checkout_cart(p_cart_id UUID)
RETURNS INTEGER AS $
DECLARE
    v_order_id INTEGER;
    v_customer_id INTEGER;
    v_total_amount DECIMAL(10,2) := 0;
    cart_item RECORD;
BEGIN
    -- 장바구니 정보 조회
    SELECT customer_id INTO v_customer_id
    FROM carts WHERE id = p_cart_id;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Cart not found: %', p_cart_id;
    END IF;

    -- 재고 확인 및 총액 계산
    FOR cart_item IN 
        SELECT ci.product_id, ci.quantity, p.price, p.inventory_count
        FROM cart_items ci
        JOIN products p ON ci.product_id = p.id
        WHERE ci.cart_id = p_cart_id
    LOOP
        IF cart_item.inventory_count &amp;lt; cart_item.quantity THEN
            RAISE EXCEPTION 'Insufficient inventory for product %', cart_item.product_id;
        END IF;

        v_total_amount := v_total_amount + (cart_item.quantity * cart_item.price);
    END LOOP;

    -- 주문 생성
    INSERT INTO orders (customer_id, total_amount)
    VALUES (v_customer_id, v_total_amount)
    RETURNING id INTO v_order_id;

    -- 주문 아이템 생성 및 재고 차감
    FOR cart_item IN 
        SELECT ci.product_id, ci.quantity, p.price
        FROM cart_items ci
        JOIN products p ON ci.product_id = p.id
        WHERE ci.cart_id = p_cart_id
    LOOP
        INSERT INTO order_items (order_id, product_id, quantity, unit_price)
        VALUES (v_order_id, cart_item.product_id, cart_item.quantity, cart_item.price);

        UPDATE products 
        SET inventory_count = inventory_count - cart_item.quantity
        WHERE id = cart_item.product_id;
    END LOOP;

    -- 장바구니 삭제
    DELETE FROM carts WHERE id = p_cart_id;

    RETURN v_order_id;
END;
$ LANGUAGE plpgsql;

-- 사용 예시
SELECT checkout_cart('cart-uuid-here');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11.3 분석 및 리포팅 패턴&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 비즈니스 인텔리전스 쿼리 예시
-- 1. 코호트 분석 (고객 리텐션)
WITH first_orders AS (
    SELECT 
        customer_id,
        MIN(DATE_TRUNC('month', created_at)) as first_order_month
    FROM orders
    GROUP BY customer_id
),
order_months AS (
    SELECT 
        o.customer_id,
        fo.first_order_month,
        DATE_TRUNC('month', o.created_at) as order_month,
        EXTRACT(YEAR FROM AGE(DATE_TRUNC('month', o.created_at), fo.first_order_month)) * 12 + 
        EXTRACT(MONTH FROM AGE(DATE_TRUNC('month', o.created_at), fo.first_order_month)) as months_since_first
    FROM orders o
    JOIN first_orders fo ON o.customer_id = fo.customer_id
)
SELECT 
    first_order_month,
    months_since_first,
    COUNT(DISTINCT customer_id) as customers,
    ROUND(100.0 * COUNT(DISTINCT customer_id) / 
          COUNT(DISTINCT customer_id) FILTER (WHERE months_since_first = 0), 2) as retention_rate
FROM order_months
WHERE first_order_month &amp;gt;= '2024-01-01'
GROUP BY first_order_month, months_since_first
ORDER BY first_order_month, months_since_first;

-- 2. RFM 분석 (Recency, Frequency, Monetary)
WITH customer_metrics AS (
    SELECT 
        customer_id,
        CURRENT_DATE - MAX(created_at::DATE) as recency_days,
        COUNT(*) as frequency,
        SUM(total_amount) as monetary_value
    FROM orders
    WHERE created_at &amp;gt;= CURRENT_DATE - INTERVAL '1 year'
    GROUP BY customer_id
),
rfm_scores AS (
    SELECT 
        customer_id,
        recency_days,
        frequency,
        monetary_value,
        NTILE(5) OVER (ORDER BY recency_days) as recency_score,
        NTILE(5) OVER (ORDER BY frequency DESC) as frequency_score,
        NTILE(5) OVER (ORDER BY monetary_value DESC) as monetary_score
    FROM customer_metrics
)
SELECT 
    CASE 
        WHEN recency_score &amp;gt;= 4 AND frequency_score &amp;gt;= 4 AND monetary_score &amp;gt;= 4 THEN 'Champions'
        WHEN recency_score &amp;gt;= 3 AND frequency_score &amp;gt;= 3 AND monetary_score &amp;gt;= 3 THEN 'Loyal Customers'
        WHEN recency_score &amp;gt;= 3 AND frequency_score &amp;lt;= 2 THEN 'Potential Loyalists'
        WHEN recency_score &amp;lt;= 2 AND frequency_score &amp;gt;= 3 THEN 'At Risk'
        WHEN recency_score &amp;lt;= 2 AND frequency_score &amp;lt;= 2 THEN 'Lost Customers'
        ELSE 'Other'
    END as customer_segment,
    COUNT(*) as customer_count,
    ROUND(AVG(monetary_value), 2) as avg_monetary_value
FROM rfm_scores
GROUP BY 1
ORDER BY customer_count DESC;

-- 3. 매출 예측 (선형 회귀)
WITH daily_sales AS (
    SELECT 
        created_at::DATE as sale_date,
        SUM(total_amount) as daily_revenue
    FROM orders
    WHERE created_at &amp;gt;= CURRENT_DATE - INTERVAL '90 days'
    GROUP BY 1
),
regression_data AS (
    SELECT 
        sale_date,
        daily_revenue,
        EXTRACT(EPOCH FROM sale_date - MIN(sale_date) OVER()) / 86400 as day_number
    FROM daily_sales
),
regression_stats AS (
    SELECT 
        COUNT(*) as n,
        AVG(day_number) as avg_x,
        AVG(daily_revenue) as avg_y,
        SUM((day_number - AVG(day_number) OVER()) * (daily_revenue - AVG(daily_revenue) OVER())) as sum_xy,
        SUM(POWER(day_number - AVG(day_number) OVER(), 2)) as sum_xx
    FROM regression_data
)
SELECT 
    'Revenue Prediction' as metric,
    avg_y + (sum_xy / sum_xx) * (90 + 7) as predicted_revenue_week_ahead,  -- 7일 후 예측
    sum_xy / sum_xx as daily_growth_rate,
    avg_y as current_avg_daily_revenue
FROM regression_stats;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;12. PostgreSQL 개발 모범 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.1 스키마 설계 원칙&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 정규화와 비정규화의 균형
-- 정규화된 설계 (OLTP 최적화)
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    email TEXT UNIQUE NOT NULL,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE addresses (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(id),
    type TEXT CHECK (type IN ('billing', 'shipping')),
    street TEXT NOT NULL,
    city TEXT NOT NULL,
    country TEXT NOT NULL,
    postal_code TEXT,
    is_default BOOLEAN DEFAULT FALSE
);

-- 비정규화된 설계 (분석 최적화)
CREATE TABLE customer_summary (
    customer_id INTEGER PRIMARY KEY,
    email TEXT,
    full_name TEXT,
    total_orders INTEGER DEFAULT 0,
    total_spent DECIMAL(12,2) DEFAULT 0,
    last_order_date TIMESTAMP,
    preferred_shipping_address JSONB,
    customer_tier TEXT,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 2. 제약 조건 활용
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    sku TEXT UNIQUE NOT NULL CHECK (LENGTH(sku) &amp;gt;= 3),
    name TEXT NOT NULL CHECK (LENGTH(TRIM(name)) &amp;gt; 0),
    price DECIMAL(10,2) NOT NULL CHECK (price &amp;gt; 0),
    category_id INTEGER NOT NULL,
    status TEXT DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'discontinued')),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- 복합 제약 조건
    CONSTRAINT valid_price_range CHECK (
        CASE 
            WHEN category_id = 1 THEN price BETWEEN 10 AND 10000  -- 전자제품
            WHEN category_id = 2 THEN price BETWEEN 5 AND 1000    -- 도서
            ELSE price &amp;gt; 0
        END
    )
);

-- 3. 적절한 데이터 타입 선택
CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,  -- 대용량 데이터 예상 시
    event_type SMALLINT NOT NULL,  -- 제한된 값들
    user_id INTEGER,  -- 일반적인 ID
    session_id UUID,  -- 고유 식별자
    timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,  -- 시간대 정보 포함
    properties JSONB,  -- 반구조화 데이터
    ip_address INET,  -- IP 주소 전용 타입
    user_agent TEXT,  -- 가변 길이 텍스트

    -- 적절한 인덱스
    INDEX idx_events_user_timestamp (user_id, timestamp),
    INDEX idx_events_type_timestamp (event_type, timestamp),
    INDEX idx_events_properties (properties) USING gin
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.2 쿼리 최적화 기법&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 효율적인 JOIN 패턴
-- 비효율적인 쿼리 (N+1 문제)
-- SELECT * FROM orders WHERE customer_id = 1;
-- SELECT * FROM order_items WHERE order_id = 123; (각 주문마다 반복)

-- 효율적인 쿼리 (JOIN 사용)
SELECT 
    o.id as order_id,
    o.total_amount,
    o.created_at,
    ARRAY_AGG(
        JSON_BUILD_OBJECT(
            'product_id', oi.product_id,
            'quantity', oi.quantity,
            'unit_price', oi.unit_price,
            'product_name', p.name
        )
    ) as items
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.id
WHERE o.customer_id = 1
GROUP BY o.id, o.total_amount, o.created_at
ORDER BY o.created_at DESC;

-- 2. 조건절 최적화
-- 비효율적: 함수 사용으로 인덱스 사용 불가
-- SELECT * FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024;

-- 효율적: 범위 조건으로 인덱스 활용
SELECT * FROM orders 
WHERE created_at &amp;gt;= '2024-01-01' 
  AND created_at &amp;lt; '2025-01-01';

-- 3. EXISTS vs IN 최적화
-- 대용량 데이터에서 EXISTS가 더 효율적
SELECT c.id, c.email
FROM customers c
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.customer_id = c.id 
      AND o.created_at &amp;gt;= CURRENT_DATE - INTERVAL '30 days'
);

-- 4. LIMIT과 OFFSET 최적화
-- 비효율적: 큰 OFFSET
-- SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 10000;

-- 효율적: 커서 기반 페이징
SELECT * FROM orders 
WHERE created_at &amp;lt; '2024-01-15 10:00:00'  -- 마지막 조회 시점
ORDER BY created_at DESC 
LIMIT 20;

-- 5. 집계 쿼리 최적화
-- 부분 인덱스를 활용한 조건부 집계
CREATE INDEX idx_orders_recent_total ON orders(created_at, total_amount) 
WHERE created_at &amp;gt;= '2024-01-01';

-- 윈도우 함수로 효율적인 순위 계산
SELECT 
    customer_id,
    total_amount,
    created_at,
    RANK() OVER (PARTITION BY customer_id ORDER BY total_amount DESC) as amount_rank
FROM orders
WHERE created_at &amp;gt;= '2024-01-01';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.3 트랜잭션 관리 패턴&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 적절한 격리 수준 선택
-- 읽기 전용 분석 쿼리 (더 나은 성능)
BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT COUNT(*) FROM large_table WHERE status = 'active';
COMMIT;

-- 일관성이 중요한 비즈니스 로직
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 복잡한 비즈니스 로직
COMMIT;

-- 2. 세이브포인트 활용
BEGIN;
    INSERT INTO customers (email, first_name, last_name) 
    VALUES ('test@example.com', 'Test', 'User');

    SAVEPOINT after_customer;

    BEGIN
        INSERT INTO addresses (customer_id, type, street, city, country) 
        VALUES (currval('customers_id_seq'), 'billing', '123 Main St', 'City', 'Country');
    EXCEPTION 
        WHEN OTHERS THEN
            ROLLBACK TO SAVEPOINT after_customer;
            -- 고객은 생성되었지만 주소는 실패
    END;

COMMIT;

-- 3. 데드락 방지 패턴
-- 일관된 순서로 리소스 접근
CREATE OR REPLACE FUNCTION transfer_funds(
    from_account_id INTEGER,
    to_account_id INTEGER,
    amount DECIMAL(10,2)
) RETURNS VOID AS $
DECLARE
    first_account INTEGER;
    second_account INTEGER;
BEGIN
    -- 데드락 방지를 위해 항상 작은 ID부터 잠금
    IF from_account_id &amp;lt; to_account_id THEN
        first_account := from_account_id;
        second_account := to_account_id;
    ELSE
        first_account := to_account_id;
        second_account := from_account_id;
    END IF;

    -- 순서대로 잠금 획득
    PERFORM balance FROM accounts WHERE id = first_account FOR UPDATE;
    PERFORM balance FROM accounts WHERE id = second_account FOR UPDATE;

    -- 잔액 확인
    IF (SELECT balance FROM accounts WHERE id = from_account_id) &amp;lt; amount THEN
        RAISE EXCEPTION 'Insufficient funds';
    END IF;

    -- 이체 실행
    UPDATE accounts SET balance = balance - amount WHERE id = from_account_id;
    UPDATE accounts SET balance = balance + amount WHERE id = to_account_id;

    -- 이체 기록
    INSERT INTO transactions (from_account_id, to_account_id, amount, transaction_type)
    VALUES (from_account_id, to_account_id, amount, 'transfer');
END;
$ LANGUAGE plpgsql;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;12.4 애플리케이션 통합 패턴&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# Python에서 PostgreSQL 고급 기능 활용 예시
import psycopg2
import psycopg2.extras
import json
from contextlib import contextmanager

class DatabaseManager:
    def __init__(self, connection_string):
        self.connection_string = connection_string

    @contextmanager
    def get_connection(self):
        conn = psycopg2.connect(self.connection_string)
        try:
            yield conn
        except Exception:
            conn.rollback()
            raise
        else:
            conn.commit()
        finally:
            conn.close()

    def execute_with_json(self, query, params=None):
        &quot;&quot;&quot;JSONB 데이터 처리 예시&quot;&quot;&quot;
        with self.get_connection() as conn:
            with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
                cursor.execute(query, params)
                return cursor.fetchall()

    def bulk_insert_json(self, data_list):
        &quot;&quot;&quot;대량 JSON 데이터 삽입&quot;&quot;&quot;
        with self.get_connection() as conn:
            with conn.cursor() as cursor:
                # COPY를 사용한 고성능 삽입
                cursor.execute(&quot;&quot;&quot;
                    CREATE TEMP TABLE temp_data (data JSONB)
                &quot;&quot;&quot;)

                # psycopg2의 execute_values 사용
                psycopg2.extras.execute_values(
                    cursor,
                    &quot;INSERT INTO temp_data (data) VALUES %s&quot;,
                    [(json.dumps(data),) for data in data_list],
                    template=None,
                    page_size=1000
                )

                # 최종 테이블로 이동
                cursor.execute(&quot;&quot;&quot;
                    INSERT INTO events (properties, created_at)
                    SELECT data, CURRENT_TIMESTAMP
                    FROM temp_data
                &quot;&quot;&quot;)

    def get_customer_analytics(self, customer_id):
        &quot;&quot;&quot;복합 분석 쿼리 실행&quot;&quot;&quot;
        query = &quot;&quot;&quot;
        WITH customer_stats AS (
            SELECT 
                COUNT(*) as total_orders,
                SUM(total_amount) as total_spent,
                AVG(total_amount) as avg_order_value,
                MAX(created_at) as last_order_date
            FROM orders 
            WHERE customer_id = %s
        ),
        recent_activity AS (
            SELECT 
                COUNT(*) as recent_orders,
                SUM(total_amount) as recent_spent
            FROM orders 
            WHERE customer_id = %s 
              AND created_at &amp;gt;= CURRENT_DATE - INTERVAL '30 days'
        )
        SELECT 
            cs.*,
            ra.recent_orders,
            ra.recent_spent,
            CASE 
                WHEN cs.total_spent &amp;gt;= 10000 THEN 'VIP'
                WHEN cs.total_spent &amp;gt;= 5000 THEN 'Gold'
                WHEN cs.total_spent &amp;gt;= 1000 THEN 'Silver'
                ELSE 'Bronze'
            END as customer_tier
        FROM customer_stats cs
        CROSS JOIN recent_activity ra
        &quot;&quot;&quot;

        with self.get_connection() as conn:
            with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
                cursor.execute(query, (customer_id, customer_id))
                return cursor.fetchone()

# 사용 예시
db = DatabaseManager(&quot;postgresql://user:pass@localhost/dbname&quot;)

# JSON 데이터 쿼리
results = db.execute_with_json(&quot;&quot;&quot;
    SELECT id, properties-&amp;gt;&amp;gt;'event_type' as event_type, created_at
    FROM events 
    WHERE properties @&amp;gt; %s
    ORDER BY created_at DESC
    LIMIT 10
&quot;&quot;&quot;, [json.dumps({&quot;user_id&quot;: 12345})])

# 고객 분석 데이터 조회
analytics = db.get_customer_analytics(12345)
print(f&quot;Customer tier: {analytics['customer_tier']}&quot;)
print(f&quot;Total spent: ${analytics['total_spent']}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;13. 문제 해결 및 디버깅&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.1 일반적인 성능 문제 진단&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 느린 쿼리 식별
-- postgresql.conf에서 설정
-- log_min_duration_statement = 1000  -- 1초 이상 쿼리 로깅
-- log_statement = 'all'  -- 모든 쿼리 로깅 (개발 환경만)

-- 현재 실행 중인 느린 쿼리 확인
SELECT 
    pid,
    now() - pg_stat_activity.query_start AS duration,
    query,
    state,
    wait_event,
    wait_event_type
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) &amp;gt; interval '1 minute'
  AND state = 'active'
ORDER BY duration DESC;

-- 2. 잠금 경합 분석
SELECT 
    l.locktype,
    l.database,
    l.relation::regclass,
    l.page,
    l.tuple,
    l.pid,
    a.query,
    l.mode,
    l.granted
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT l.granted
ORDER BY l.pid;

-- 3. 인덱스 사용률 분석
SELECT 
    schemaname,
    tablename,
    attname as column_name,
    n_distinct,
    correlation,
    most_common_vals,
    most_common_freqs
FROM pg_stats
WHERE schemaname = 'public'
  AND tablename = 'orders'
ORDER BY n_distinct DESC;

-- 4. 테이블 bloat 분석
SELECT 
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size,
    pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) as table_size,
    n_dead_tup,
    n_live_tup,
    ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) as dead_tuple_percent,
    last_vacuum,
    last_autovacuum
FROM pg_stat_user_tables
WHERE n_dead_tup &amp;gt; 1000
ORDER BY dead_tuple_percent DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.2 메모리 및 캐시 최적화&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 버퍼 캐시 분석
SELECT 
    c.relname,
    pg_size_pretty(count(*) * 8192) as buffered,
    round(100.0 * count(*) / 
        (SELECT setting FROM pg_settings WHERE name='shared_buffers')::integer, 1) as buffer_percent,
    round(100.0 * count(*) * 8192 / pg_relation_size(c.oid), 1) as percent_of_relation
FROM pg_class c
INNER JOIN pg_buffercache b ON b.relfilenode = c.relfilenode
INNER JOIN pg_database d ON (b.reldatabase = d.oid AND d.datname = current_database())
GROUP BY c.oid, c.relname
ORDER BY 2 DESC
LIMIT 20;

-- 2. 쿼리 플랜 캐시 상태
SELECT 
    query,
    calls,
    total_time,
    rows,
    100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent,
    pg_size_pretty(temp_blks_written * 8192) as temp_written
FROM pg_stat_statements
WHERE calls &amp;gt; 100
ORDER BY temp_blks_written DESC
LIMIT 20;

-- 3. 작업 메모리 사용량 최적화
-- work_mem 설정 최적화를 위한 분석
SELECT 
    query,
    calls,
    total_time / calls as avg_time,
    temp_blks_written,
    temp_blks_read
FROM pg_stat_statements
WHERE temp_blks_written &amp;gt; 0
ORDER BY temp_blks_written DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;13.3 연결 및 리소스 관리&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 연결 상태 모니터링
SELECT 
    state,
    COUNT(*) as connection_count,
    MAX(now() - state_change) as max_age
FROM pg_stat_activity
GROUP BY state
ORDER BY connection_count DESC;

-- 2. 유휴 연결 정리
SELECT 
    pid,
    usename,
    application_name,
    client_addr,
    state,
    state_change,
    now() - state_change as idle_duration
FROM pg_stat_activity
WHERE state = 'idle'
  AND (now() - state_change) &amp;gt; interval '30 minutes'
ORDER BY idle_duration DESC;

-- 위험한 유휴 연결 강제 종료 (주의깊게 사용)
-- SELECT pg_terminate_backend(pid) 
-- FROM pg_stat_activity 
-- WHERE state = 'idle' 
--   AND (now() - state_change) &amp;gt; interval '1 hour'
--   AND usename != 'postgres';

-- 3. 자원 사용량 모니터링
SELECT 
    datname,
    numbackends as active_connections,
    xact_commit,
    xact_rollback,
    blks_read,
    blks_hit,
    temp_files,
    temp_bytes,
    deadlocks
FROM pg_stat_database
WHERE datname = current_database();&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;14. 마이그레이션 및 업그레이드 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.1 무중단 마이그레이션 기법&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. 단계별 컬럼 추가 마이그레이션
-- 1단계: NOT NULL 제약 없이 컬럼 추가
ALTER TABLE large_table ADD COLUMN new_status TEXT;

-- 2단계: 기본값으로 기존 데이터 업데이트 (배치 처리)
DO $
DECLARE
    batch_size INTEGER := 1000;
    rows_updated INTEGER;
BEGIN
    LOOP
        UPDATE large_table 
        SET new_status = 'active'
        WHERE new_status IS NULL
          AND id IN (
              SELECT id FROM large_table 
              WHERE new_status IS NULL 
              LIMIT batch_size
          );

        GET DIAGNOSTICS rows_updated = ROW_COUNT;
        EXIT WHEN rows_updated = 0;

        -- 다른 트랜잭션이 실행될 수 있도록 잠시 대기
        PERFORM pg_sleep(0.1);
    END LOOP;
END $;

-- 3단계: NOT NULL 제약 추가
ALTER TABLE large_table ALTER COLUMN new_status SET NOT NULL;

-- 4단계: 기존 컬럼 제거 (필요시)
-- ALTER TABLE large_table DROP COLUMN old_status;

-- 2. 인덱스 동시 생성 (CREATE INDEX CONCURRENTLY)
CREATE INDEX CONCURRENTLY idx_large_table_new_status 
ON large_table (new_status);

-- 실패한 동시 인덱스 정리
-- DROP INDEX CONCURRENTLY idx_large_table_new_status;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.2 데이터 검증 및 무결성 체크&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1. 마이그레이션 후 데이터 검증
CREATE OR REPLACE FUNCTION validate_migration()
RETURNS TABLE(
    check_name TEXT,
    status TEXT,
    details TEXT
) AS $
BEGIN
    -- 레코드 수 확인
    RETURN QUERY 
    SELECT 
        'record_count'::TEXT,
        CASE WHEN COUNT(*) &amp;gt; 0 THEN 'PASS' ELSE 'FAIL' END::TEXT,
        'Total records: ' || COUNT(*)::TEXT
    FROM customers;

    -- 외래 키 무결성 확인
    RETURN QUERY
    SELECT 
        'foreign_key_integrity'::TEXT,
        CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END::TEXT,
        'Orphaned orders: ' || COUNT(*)::TEXT
    FROM orders o
    LEFT JOIN customers c ON o.customer_id = c.id
    WHERE c.id IS NULL;

    -- 데이터 타입 일관성 확인
    RETURN QUERY
    SELECT 
        'data_consistency'::TEXT,
        CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END::TEXT,
        'Invalid email formats: ' || COUNT(*)::TEXT
    FROM customers
    WHERE email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,};

    -- JSON 데이터 유효성 확인
    RETURN QUERY
    SELECT 
        'json_validity'::TEXT,
        CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END::TEXT,
        'Invalid JSON properties: ' || COUNT(*)::TEXT
    FROM products
    WHERE specifications IS NOT NULL 
      AND NOT (specifications::TEXT ~ '^{.*});

END;
$ LANGUAGE plpgsql;

-- 검증 실행
SELECT * FROM validate_migration();&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;14.3 버전 업그레이드 준비&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 1. PostgreSQL 호환성 체크
-- pg_upgrade 전 호환성 확인
-- pg_upgrade --check -b /usr/lib/postgresql/13/bin -B /usr/lib/postgresql/14/bin -d /var/lib/postgresql/13/main -D /var/lib/postgresql/14/main

-- 2. 확장 모듈 호환성 확인
SELECT 
    extname,
    extversion,
    extrelocatable
FROM pg_extension
ORDER BY extname;

-- 3. 사용자 정의 함수 호환성 체크
SELECT 
    n.nspname as schema_name,
    p.proname as function_name,
    pg_get_function_identity_arguments(p.oid) as arguments,
    l.lanname as language
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
JOIN pg_language l ON p.prolang = l.oid
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
  AND l.lanname != 'internal'
ORDER BY n.nspname, p.proname;

-- 4. 통계 정보 백업 (업그레이드 후 복원용)
CREATE TABLE pg_stats_backup AS
SELECT * FROM pg_stats;

-- 업그레이드 후 통계 복원을 위한 스크립트 생성
SELECT 
    'ALTER TABLE ' || schemaname || '.' || tablename || 
    ' ALTER COLUMN ' || attname || ' SET STATISTICS ' || 
    COALESCE(n_distinct::text, 'DEFAULT') || ';'
FROM pg_stats_backup
WHERE schemaname = 'public'
  AND n_distinct IS NOT NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;15. 결론 및 향후 방향&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 단순한 관계형 데이터베이스를 넘어서 현대적인 애플리케이션 개발에 필요한 거의 모든 기능을 제공하는 종합적인 데이터 플랫폼으로 발전했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 강점 요약:&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;: 사용자 정의 타입, 함수, 연산자를 통한 무한 확장 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표준 준수&lt;/b&gt;: SQL 표준을 엄격히 따르면서도 혁신적인 기능 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능&lt;/b&gt;: MVCC, 고급 인덱싱, 쿼리 최적화를 통한 뛰어난 성능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;안정성&lt;/b&gt;: ACID 트랜잭션과 강력한 데이터 무결성 보장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비용 효율성&lt;/b&gt;: 오픈소스로 제공되는 엔터프라이즈급 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습 로드맵:&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;기초&lt;/b&gt;: SQL 문법, 기본 데이터 타입, 인덱스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중급&lt;/b&gt;: 고급 데이터 타입(JSONB, Arrays), 윈도우 함수, CTE&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고급&lt;/b&gt;: 확장 개발, 성능 튜닝, 고가용성 구성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전문가&lt;/b&gt;: 커스텀 확장 개발, 대규모 시스템 아키텍처 설계&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지속적인 발전:&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 매년 새로운 기능과 성능 개선을 제공하며, 클라우드 네이티브 환경, 분산 시스템, AI/ML 워크로드 등 새로운 요구사항에 지속적으로 적응하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드의 예제들을 실습해보고, 실제 프로젝트에 적용해보시기 바랍니다. PostgreSQL의 진정한 힘은 이론이 아닌 실무에서 발휘됩니다!&lt;/p&gt;</description>
      <category>DB/PostgreSQL</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/138</guid>
      <comments>https://devsite.tistory.com/entry/PostgreSQL-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C#entry138comment</comments>
      <pubDate>Mon, 25 Aug 2025 10:14:22 +0900</pubDate>
    </item>
    <item>
      <title>RESTful API 완벽 가이드 - 개발자가 알아야 할 핵심 개념</title>
      <link>https://devsite.tistory.com/entry/h1RESTful-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90h1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;intro&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RESTful API&lt;/b&gt;는 현재 웹 개발의 표준이 되었습니다. 구글, 페이스북, 트위터 등 거대 IT 기업들이 모두 RESTful 설계 원칙을 따르고 있으며, 개발자라면 반드시 알아야 할 필수 개념입니다. 하지만 정확히 RESTful이 무엇인지, 왜 이렇게 중요한지 궁금하지 않으신가요? 단순히 HTTP 메서드만 사용하면 RESTful API일까요?&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. RESTful API란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RESTful API&lt;/b&gt;는 REST(Representational State Transfer) 아키텍처 스타일을 따르는 API를 의미합니다. 2000년 로이 필딩(Roy Fielding)이 박사 논문에서 제시한 개념으로, 웹의 기존 기술과 HTTP 프로토콜을 그대로 활용하여 웹의 장점을 최대한 활용할 수 있는 아키텍처입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 &lt;b&gt;자원(Resource), 행위(Verb), 표현(Representation)&lt;/b&gt;이라는 3가지 요소로 구성됩니다. 예를 들어 사용자 정보를 조회한다면, '사용자'는 자원, 'GET'은 행위, 'JSON 형태의 데이터'는 표현에 해당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 중요한 특징은 &lt;b&gt;무상태성(Stateless)&lt;/b&gt;입니다. 각 요청은 독립적이며, 서버는 클라이언트의 상태를 저장하지 않습니다. 이로 인해 서버의 확장성이 크게 향상됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. RESTful 설계 원칙과 실제 구현&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RESTful API 설계에는 &lt;b&gt;6가지 핵심 원칙&lt;/b&gt;이 있습니다. 첫째, &lt;b&gt;균등한 인터페이스(Uniform Interface)&lt;/b&gt;로 일관된 방식으로 자원에 접근해야 합니다. 둘째, &lt;b&gt;무상태성&lt;/b&gt;으로 각 요청은 완전한 정보를 포함해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현 예시를 살펴보면, 사용자 관리 API의 경우 다음과 같습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- GET /users (모든 사용자 조회)&lt;br /&gt;- GET /users/123 (특정 사용자 조회)&lt;br /&gt;- POST /users (새 사용자 생성)&lt;br /&gt;- PUT /users/123 (사용자 정보 전체 수정)&lt;br /&gt;- DELETE /users/123 (사용자 삭제)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;캐시 가능성(Cacheable)&lt;/b&gt;과 &lt;b&gt;계층화 시스템(Layered System)&lt;/b&gt; 원칙도 중요합니다. GET 요청의 응답은 캐시 가능하며, 클라이언트는 서버의 내부 구조를 알 필요가 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. HTTP 메서드와 상태 코드 활용법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP 메서드&lt;/b&gt;는 RESTful API의 핵심입니다. &lt;b&gt;GET&lt;/b&gt;은 데이터 조회, &lt;b&gt;POST&lt;/b&gt;는 새 자원 생성, &lt;b&gt;PUT&lt;/b&gt;은 자원 전체 수정, &lt;b&gt;PATCH&lt;/b&gt;는 부분 수정, &lt;b&gt;DELETE&lt;/b&gt;는 자원 삭제에 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태 코드 역시 중요한 역할을 합니다. &lt;b&gt;200 OK&lt;/b&gt;는 성공적인 요청, &lt;b&gt;201 Created&lt;/b&gt;는 자원 생성 성공, &lt;b&gt;400 Bad Request&lt;/b&gt;는 잘못된 요청, &lt;b&gt;404 Not Found&lt;/b&gt;는 자원 없음, &lt;b&gt;500 Internal Server Error&lt;/b&gt;는 서버 오류를 나타냅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 &lt;b&gt;POST와 PUT의 차이&lt;/b&gt;를 정확히 구분해야 합니다. POST는 서버가 자원의 식별자를 생성하고, PUT은 클라이언트가 식별자를 지정합니다. 예를 들어 POST /users는 서버가 새 사용자 ID를 할당하지만, PUT /users/123은 클라이언트가 123번 사용자를 지정합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. RESTful API의 장점과 실제 사례&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RESTful API의 가장 큰 장점&lt;/b&gt;은 &lt;b&gt;단순성과 확장성&lt;/b&gt;입니다. HTTP 표준을 따르기 때문에 별도의 프로토콜을 배울 필요가 없고, 다양한 클라이언트(웹, 모바일, IoT)에서 동일한 API를 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트위터 API&lt;/b&gt;는 RESTful 설계의 대표적인 사례입니다. GET /tweets로 트윗 목록을 조회하고, POST /tweets로 새 트윗을 작성하며, DELETE /tweets/:id로 특정 트윗을 삭제합니다. 이러한 직관적인 구조 덕분에 개발자들이 쉽게 이해하고 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GitHub API&lt;/b&gt; 역시 훌륭한 예시입니다. GET /repositories, POST /repositories, GET /repositories/:id/issues 등 일관된 패턴을 유지하여 개발자 경험을 크게 향상시켰습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. RESTful API 설계 시 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RESTful API 설계&lt;/b&gt;에서 흔히 하는 실수들이 있습니다. 첫째, &lt;b&gt;동사형 URL 사용&lt;/b&gt;입니다. /getUsers, /createUser 대신 /users와 HTTP 메서드를 조합해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;b&gt;일관성 없는 명명 규칙&lt;/b&gt;입니다. /users와 /user-profiles처럼 혼재하지 말고, 케밥케이스나 스네이크케이스 중 하나로 통일해야 합니다. 셋째, &lt;b&gt;버전 관리 부재&lt;/b&gt;입니다. /v1/users, /v2/users처럼 명시적인 버전 관리가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;보안 측면&lt;/b&gt;에서는 HTTPS 사용이 필수이며, 인증과 권한 부여를 위해 &lt;b&gt;JWT(JSON Web Token)&lt;/b&gt;나 &lt;b&gt;OAuth 2.0&lt;/b&gt;을 활용해야 합니다. 또한 적절한 &lt;b&gt;Rate Limiting&lt;/b&gt;을 통해 API 남용을 방지해야 합니다.&lt;/p&gt;
&lt;div class=&quot;conclusion&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RESTful API&lt;/b&gt;는 단순한 기술이 아닌 웹 개발의 철학입니다. HTTP의 기본 원칙을 따르면서도 확장 가능하고 유지보수가 쉬운 시스템을 구축할 수 있게 해줍니다. 올바른 설계 원칙을 따르고 일관성을 유지한다면, 개발자와 사용자 모두에게 편리한 API를 만들 수 있습니다. 앞으로도 RESTful API는 웹 개발의 핵심 기술로 자리잡을 것으로 전망됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;div class=&quot;terms&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[전문용어]&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;REST&lt;/b&gt;: 웹의 기존 기술을 활용한 아키텍처 설계 원칙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API&lt;/b&gt;: 응용 프로그램 간 데이터를 주고받는 인터페이스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP 메서드&lt;/b&gt;: GET, POST, PUT, DELETE 등 HTTP 요청의 종류&lt;/li&gt;
&lt;li&gt;&lt;b&gt;무상태성&lt;/b&gt;: 서버가 클라이언트의 상태 정보를 보관하지 않는 특성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JSON&lt;/b&gt;: 데이터 교환을 위한 경량 텍스트 기반 형식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JWT&lt;/b&gt;: 정보를 안전하게 전송하기 위한 토큰 기반 인증 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OAuth 2.0&lt;/b&gt;: 제3자 응용 프로그램의 접근 권한을 관리하는 표준 프로토콜&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>언어/JAVA</category>
      <category>API 설계</category>
      <category>HTTP 메서드</category>
      <category>HTTP 상태코드</category>
      <category>json</category>
      <category>REST 아키텍처</category>
      <category>restful api</category>
      <category>무상태성</category>
      <category>백엔드 개발</category>
      <category>웹 개발</category>
      <category>웹 서비스</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/137</guid>
      <comments>https://devsite.tistory.com/entry/h1RESTful-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90h1#entry137comment</comments>
      <pubDate>Wed, 23 Jul 2025 23:04:34 +0900</pubDate>
    </item>
    <item>
      <title>Spring/JPA 데이터베이스 관련 어노테이션 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/SpringJPA-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B4%80%EB%A0%A8-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    
    &lt;style&gt;
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f7fa;
            color: #333;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }
        h1 {
            color: #2c3e50;
            border-bottom: 4px solid #3498db;
            padding-bottom: 15px;
            margin-bottom: 30px;
            text-align: center;
        }
        h2 {
            color: #34495e;
            margin-top: 40px;
            margin-bottom: 20px;
            border-left: 5px solid #3498db;
            padding-left: 15px;
            background: #ecf0f1;
            padding: 10px 15px;
            border-radius: 5px;
        }
        h3 {
            color: #2980b9;
            margin-top: 25px;
            margin-bottom: 15px;
            border-bottom: 2px solid #bdc3c7;
            padding-bottom: 5px;
        }
        .annotation {
            background: #f8f9fa;
            border: 1px solid #e9ecef;
            border-radius: 8px;
            padding: 20px;
            margin: 20px 0;
        }
        .annotation-name {
            font-size: 18px;
            font-weight: bold;
            color: #e74c3c;
            margin-bottom: 10px;
        }
        .code-block {
            background: #2c3e50;
            color: #ecf0f1;
            padding: 15px;
            border-radius: 6px;
            overflow-x: auto;
            margin: 10px 0;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 13px;
        }
        .small-code {
            background: #34495e;
            color: #ecf0f1;
            padding: 8px;
            border-radius: 4px;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 12px;
            display: inline-block;
        }
        .purpose {
            background: #e8f5e8;
            border-left: 4px solid #27ae60;
            padding: 10px 15px;
            margin: 10px 0;
            border-radius: 4px;
        }
        .warning {
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 10px 15px;
            margin: 10px 0;
            border-radius: 4px;
        }
        .danger {
            background: #f8d7da;
            border-left: 4px solid #dc3545;
            padding: 10px 15px;
            margin: 10px 0;
            border-radius: 4px;
        }
        .info {
            background: #d1ecf1;
            border-left: 4px solid #17a2b8;
            padding: 10px 15px;
            margin: 10px 0;
            border-radius: 4px;
        }
        .params {
            background: #f0f2f5;
            padding: 15px;
            border-radius: 6px;
            margin: 10px 0;
        }
        .param-item {
            margin: 8px 0;
            padding-left: 20px;
        }
        .param-name {
            font-weight: bold;
            color: #8e44ad;
        }
        ul {
            padding-left: 20px;
        }
        li {
            margin-bottom: 5px;
        }
        .toc {
            background: #f1f3f4;
            padding: 20px;
            border-radius: 8px;
            margin: 20px 0;
        }
        .toc h3 {
            margin-top: 0;
            color: #2c3e50;
        }
        .toc ul {
            margin: 0;
        }
        .toc a {
            color: #3498db;
            text-decoration: none;
        }
        .toc a:hover {
            text-decoration: underline;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class=&quot;container&quot;&gt;
        &lt;h1&gt;Spring/JPA 데이터베이스 관련 어노테이션 완벽 가이드&lt;/h1&gt;
        
        &lt;div class=&quot;toc&quot;&gt;
            &lt;h3&gt;목차&lt;/h3&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href=&quot;#transaction&quot;&gt;1. 트랜잭션 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#entity&quot;&gt;2. JPA 엔티티 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#repository&quot;&gt;3. Repository 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#validation&quot;&gt;4. 검증 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#cache&quot;&gt;5. 캐싱 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#performance&quot;&gt;6. 성능 최적화 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#test&quot;&gt;7. 테스트 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#audit&quot;&gt;8. 감사(Auditing) 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#event&quot;&gt;9. 이벤트 관련 어노테이션&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;transaction&quot;&gt;1. 트랜잭션 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Transactional&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 메서드나 클래스에 트랜잭션 경계를 설정하여 데이터베이스 작업의 원자성을 보장합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Transactional(
    isolation = Isolation.READ_COMMITTED,
    propagation = Propagation.REQUIRED,
    rollbackFor = Exception.class,
    noRollbackFor = IllegalArgumentException.class,
    timeout = 30,
    readOnly = false
)
public void updateUser(User user) {
    userRepository.save(user);
}&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;isolation:&lt;/span&gt; 트랜잭션 격리 수준 (DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;propagation:&lt;/span&gt; 트랜잭션 전파 방식 (REQUIRED, REQUIRES_NEW, NESTED, SUPPORTS 등)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;rollbackFor:&lt;/span&gt; 롤백을 수행할 예외 클래스 지정&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;timeout:&lt;/span&gt; 트랜잭션 타임아웃 시간 (초)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;readOnly:&lt;/span&gt; 읽기 전용 트랜잭션 여부&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@EnableTransactionManagement&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Spring의 트랜잭션 관리 기능을 활성화합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                Configuration 클래스에 추가하여 @Transactional 어노테이션이 동작하도록 합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;entity&quot;&gt;2. JPA 엔티티 관련 어노테이션&lt;/h2&gt;

        &lt;h3&gt;기본 엔티티 어노테이션&lt;/h3&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Entity&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 클래스를 JPA 엔티티로 지정하여 데이터베이스 테이블과 매핑합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@Table(name = &quot;users&quot;)
public class User {
    // 필드와 메서드
}&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;name:&lt;/span&gt; 엔티티 이름 (기본값: 클래스명)&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Table&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티와 매핑될 테이블의 상세 정보를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Table(
    name = &quot;users&quot;,
    schema = &quot;public&quot;,
    indexes = {
        @Index(name = &quot;idx_email&quot;, columnList = &quot;email&quot;),
        @Index(name = &quot;idx_name_status&quot;, columnList = &quot;name, status&quot;)
    },
    uniqueConstraints = @UniqueConstraint(columnNames = {&quot;email&quot;})
)
public class User { ... }&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;name:&lt;/span&gt; 테이블 이름&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;schema:&lt;/span&gt; 데이터베이스 스키마&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;indexes:&lt;/span&gt; 인덱스 정의&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;uniqueConstraints:&lt;/span&gt; 유니크 제약 조건&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Id&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티의 기본 키(Primary Key)를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;&lt;/div&gt;
            &lt;div class=&quot;warning&quot;&gt;
                모든 엔티티는 반드시 @Id 어노테이션이 지정된 필드가 하나 이상 있어야 합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@GeneratedValue&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 기본 키 값의 생성 전략을 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;user_seq&quot;)
@SequenceGenerator(name = &quot;user_seq&quot;, sequenceName = &quot;user_sequence&quot;, allocationSize = 1)
private Long id;&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 전략:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;AUTO:&lt;/span&gt; JPA 구현체가 자동으로 선택&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;IDENTITY:&lt;/span&gt; 데이터베이스의 AUTO_INCREMENT 사용&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;SEQUENCE:&lt;/span&gt; 데이터베이스 시퀀스 사용&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;TABLE:&lt;/span&gt; 별도 테이블을 사용한 키 생성&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Column&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티 필드와 데이터베이스 컬럼의 매핑 정보를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Column(
    name = &quot;email&quot;,
    nullable = false,
    unique = true,
    length = 100,
    columnDefinition = &quot;VARCHAR(100) NOT NULL&quot;
)
private String email;&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;name:&lt;/span&gt; 컬럼 이름&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;nullable:&lt;/span&gt; NULL 허용 여부&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;unique:&lt;/span&gt; 유니크 제약 조건&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;length:&lt;/span&gt; 문자열 길이&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;columnDefinition:&lt;/span&gt; 컬럼 정의 SQL&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Temporal&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 날짜/시간 타입 필드의 데이터베이스 매핑 방식을 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Temporal(TemporalType.TIMESTAMP)
private Date createdAt;

@Temporal(TemporalType.DATE)
private Date birthDate;&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;타입:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;DATE:&lt;/span&gt; 날짜만 (YYYY-MM-DD)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;TIME:&lt;/span&gt; 시간만 (HH:MM:SS)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;TIMESTAMP:&lt;/span&gt; 날짜와 시간 (YYYY-MM-DD HH:MM:SS)&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Enumerated&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Enum 타입 필드의 데이터베이스 저장 방식을 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Enumerated(EnumType.STRING)
private UserStatus status;

@Enumerated(EnumType.ORDINAL)
private Priority priority;&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;타입:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;STRING:&lt;/span&gt; Enum 이름을 문자열로 저장 (권장)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;ORDINAL:&lt;/span&gt; Enum 순서를 숫자로 저장&lt;/div&gt;
            &lt;/div&gt;
            &lt;div class=&quot;warning&quot;&gt;
                ORDINAL 사용 시 Enum 순서가 변경되면 데이터 무결성 문제가 발생할 수 있습니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Lob&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 대용량 데이터(Large Object)를 저장하는 필드를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Lob
private String content; // CLOB

@Lob
private byte[] image; // BLOB&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                문자열 타입은 CLOB, 바이트 배열은 BLOB으로 매핑됩니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;h3&gt;연관관계 어노테이션&lt;/h3&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@OneToOne&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 일대일 관계를 매핑합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = &quot;profile_id&quot;)
private UserProfile profile;&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;fetch:&lt;/span&gt; 로딩 전략 (LAZY, EAGER)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;cascade:&lt;/span&gt; 영속성 전이 설정&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;mappedBy:&lt;/span&gt; 연관관계의 주인이 아닌 쪽에서 사용&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@OneToMany&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 일대다 관계를 매핑합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@OneToMany(mappedBy = &quot;user&quot;, cascade = CascadeType.ALL, orphanRemoval = true)
private List&amp;lt;Order&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;mappedBy:&lt;/span&gt; 연관관계의 주인 지정&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;orphanRemoval:&lt;/span&gt; 고아 객체 제거 여부&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@ManyToOne&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 다대일 관계를 매핑합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;user_id&quot;)
private User user;&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                다대일 관계에서는 '다' 쪽이 연관관계의 주인이 됩니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@ManyToMany&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 다대다 관계를 매핑합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@ManyToMany
@JoinTable(
    name = &quot;user_roles&quot;,
    joinColumns = @JoinColumn(name = &quot;user_id&quot;),
    inverseJoinColumns = @JoinColumn(name = &quot;role_id&quot;)
)
private Set&amp;lt;Role&amp;gt; roles = new HashSet&amp;lt;&amp;gt;();&lt;/div&gt;
            &lt;div class=&quot;warning&quot;&gt;
                다대다 관계는 중간 테이블을 생성하며, 복잡한 비즈니스 로직이 필요한 경우 중간 엔티티를 직접 생성하는 것이 좋습니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@JoinColumn&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 연관관계 매핑 시 외래 키 컬럼을 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@JoinColumn(
    name = &quot;user_id&quot;,
    nullable = false,
    foreignKey = @ForeignKey(name = &quot;FK_order_user&quot;)
)
private User user;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@JoinTable&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 다대다 관계에서 중간 테이블을 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@JoinTable(
    name = &quot;user_roles&quot;,
    joinColumns = @JoinColumn(name = &quot;user_id&quot;),
    inverseJoinColumns = @JoinColumn(name = &quot;role_id&quot;)
)&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;repository&quot;&gt;3. Repository 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Repository&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 데이터 접근 계층을 나타내는 스테레오타입 어노테이션입니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Repository
public class UserRepositoryImpl {
    
    @PersistenceContext
    private EntityManager entityManager;
}&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                Spring의 예외 변환 기능을 제공하여 데이터베이스 예외를 Spring의 DataAccessException으로 변환합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Query&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Repository 메서드에 JPQL 또는 네이티브 SQL 쿼리를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Query(&quot;SELECT u FROM User u WHERE u.email = :email&quot;)
Optional&amp;lt;User&amp;gt; findByEmail(@Param(&quot;email&quot;) String email);

@Query(value = &quot;SELECT * FROM users WHERE created_at &gt; :date&quot;, nativeQuery = true)
List&amp;lt;User&amp;gt; findUsersCreatedAfter(@Param(&quot;date&quot;) Date date);&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;value:&lt;/span&gt; JPQL 또는 SQL 쿼리&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;nativeQuery:&lt;/span&gt; 네이티브 SQL 사용 여부&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Modifying&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 데이터 변경 쿼리(UPDATE, DELETE)임을 나타냅니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Modifying
@Query(&quot;UPDATE User u SET u.status = :status WHERE u.id = :id&quot;)
int updateUserStatus(@Param(&quot;id&quot;) Long id, @Param(&quot;status&quot;) UserStatus status);&lt;/div&gt;
            &lt;div class=&quot;danger&quot;&gt;
                @Modifying 쿼리 실행 후에는 영속성 컨텍스트를 수동으로 clear해야 할 수 있습니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Param&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 쿼리 파라미터의 이름을 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Query(&quot;SELECT u FROM User u WHERE u.name = :name AND u.age &gt; :age&quot;)
List&amp;lt;User&amp;gt; findByNameAndAge(@Param(&quot;name&quot;) String name, @Param(&quot;age&quot;) int age);&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PersistenceContext&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; JPA EntityManager를 주입합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PersistenceContext
private EntityManager entityManager;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PersistenceUnit&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; JPA EntityManagerFactory를 주입합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PersistenceUnit
private EntityManagerFactory entityManagerFactory;&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;validation&quot;&gt;4. 검증 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@NotNull&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 필드가 null이 아님을 검증합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@NotNull(message = &quot;이메일은 필수입니다&quot;)
private String email;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@NotEmpty&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 컬렉션이나 문자열이 null이 아니고 비어있지 않음을 검증합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@NotEmpty(message = &quot;이름은 필수입니다&quot;)
private String name;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@NotBlank&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 문자열이 null이 아니고 공백이 아님을 검증합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@NotBlank(message = &quot;제목은 필수입니다&quot;)
private String title;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Size&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 문자열, 컬렉션, 배열의 크기를 검증합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Size(min = 2, max = 50, message = &quot;이름은 2-50자 사이여야 합니다&quot;)
private String name;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Min / @Max&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 숫자 값의 최소/최대 값을 검증합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Min(value = 0, message = &quot;나이는 0 이상이어야 합니다&quot;)
@Max(value = 150, message = &quot;나이는 150 이하여야 합니다&quot;)
private Integer age;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Email&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 이메일 형식을 검증합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Email(message = &quot;올바른 이메일 형식이 아닙니다&quot;)
private String email;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Pattern&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 정규표현식을 이용한 패턴 검증을 수행합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Pattern(regexp = &quot;^[0-9]{10,11}$&quot;, message = &quot;올바른 전화번호 형식이 아닙니다&quot;)
private String phoneNumber;&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;cache&quot;&gt;5. 캐싱 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Cacheable&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 메서드 실행 결과를 캐시에 저장합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Cacheable(value = &quot;users&quot;, key = &quot;#id&quot;)
public User findById(Long id) {
    return userRepository.findById(id).orElse(null);
}&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;value:&lt;/span&gt; 캐시 이름&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;key:&lt;/span&gt; 캐시 키 (SpEL 표현식)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;condition:&lt;/span&gt; 캐시 조건&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@CacheEvict&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 캐시에서 데이터를 제거합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@CacheEvict(value = &quot;users&quot;, key = &quot;#user.id&quot;)
public User updateUser(User user) {
    return userRepository.save(user);
}

@CacheEvict(value = &quot;users&quot;, allEntries = true)
public void clearAllUsers() {
    // 모든 사용자 캐시 삭제
}&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;allEntries:&lt;/span&gt; 모든 캐시 엔트리 삭제 여부&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;beforeInvocation:&lt;/span&gt; 메서드 실행 전 캐시 삭제 여부&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@CachePut&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 메서드를 항상 실행하고 결과를 캐시에 저장합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@CachePut(value = &quot;users&quot;, key = &quot;#user.id&quot;)
public User updateUser(User user) {
    return userRepository.save(user);
}&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                @Cacheable과 달리 메서드를 항상 실행하고 캐시를 업데이트합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@EnableCaching&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Spring의 캐시 기능을 활성화합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager(&quot;users&quot;, &quot;orders&quot;);
    }
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;performance&quot;&gt;6. 성능 최적화 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@NamedQuery&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티에 명명된 쿼리를 정의하여 재사용합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@NamedQuery(
    name = &quot;User.findByEmailAndStatus&quot;,
    query = &quot;SELECT u FROM User u WHERE u.email = :email AND u.status = :status&quot;
)
public class User { ... }&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                애플리케이션 시작 시 쿼리 검증이 이루어져 런타임 오류를 방지할 수 있습니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@NamedEntityGraph&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티 그래프를 정의하여 N+1 문제를 해결합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@NamedEntityGraph(
    name = &quot;User.withOrders&quot;,
    attributeNodes = {
        @NamedAttributeNode(&quot;orders&quot;),
        @NamedAttributeNode(value = &quot;profile&quot;, subgraph = &quot;profile.details&quot;)
    },
    subgraphs = @NamedSubgraph(
        name = &quot;profile.details&quot;,
        attributeNodes = @NamedAttributeNode(&quot;address&quot;)
    )
)
public class User { ... }&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@EntityGraph&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Repository 메서드에서 엔티티 그래프를 사용합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    
    @EntityGraph(value = &quot;User.withOrders&quot;, type = EntityGraph.EntityGraphType.FETCH)
    List&amp;lt;User&amp;gt; findByStatus(UserStatus status);
    
    @EntityGraph(attributePaths = {&quot;orders&quot;, &quot;profile&quot;})
    Optional&amp;lt;User&amp;gt; findById(Long id);
}&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;value:&lt;/span&gt; @NamedEntityGraph 이름&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;attributePaths:&lt;/span&gt; 직접 로딩할 속성 경로&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;type:&lt;/span&gt; FETCH(즉시 로딩) 또는 LOAD(기본값 유지)&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@BatchSize&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 컬렉션이나 프록시를 일괄 로딩할 때의 배치 크기를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@OneToMany(mappedBy = &quot;user&quot;)
@BatchSize(size = 10)
private List&amp;lt;Order&amp;gt; orders;&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                N+1 문제를 완화하기 위해 지정된 크기만큼 한 번에 로딩합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Fetch&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Hibernate의 fetch 전략을 세밀하게 제어합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@OneToMany(mappedBy = &quot;user&quot;)
@Fetch(FetchMode.SUBSELECT)
private List&amp;lt;Order&amp;gt; orders;&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;FetchMode:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;SELECT:&lt;/span&gt; 기본값, 별도 SELECT 쿼리&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;JOIN:&lt;/span&gt; JOIN을 사용한 즉시 로딩&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;SUBSELECT:&lt;/span&gt; 서브쿼리를 사용한 로딩&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;test&quot;&gt;7. 테스트 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@DataJpaTest&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; JPA Repository 계층만을 테스트하기 위한 슬라이스 테스트를 제공합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserRepositoryTest {
    
    @Autowired
    private TestEntityManager entityManager;
    
    @Autowired
    private UserRepository userRepository;
}&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                내장 데이터베이스를 사용하고 @Entity 클래스들과 Repository만 스캔합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@AutoConfigureTestDatabase&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 테스트용 데이터베이스 설정을 제어합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class IntegrationTest { ... }&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;Replace 옵션:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;ANY:&lt;/span&gt; 내장 데이터베이스로 교체 (기본값)&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;AUTO_CONFIGURED:&lt;/span&gt; 자동 설정된 DataSource만 교체&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;NONE:&lt;/span&gt; 교체하지 않음&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Sql&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 테스트 실행 전후에 SQL 스크립트를 실행합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Test
@Sql(&quot;/test-data.sql&quot;)
@Sql(scripts = &quot;/cleanup.sql&quot;, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
public void testFindByEmail() { ... }&lt;/div&gt;
            &lt;div class=&quot;params&quot;&gt;
                &lt;strong&gt;주요 속성:&lt;/strong&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;scripts:&lt;/span&gt; 실행할 SQL 파일 경로&lt;/div&gt;
                &lt;div class=&quot;param-item&quot;&gt;&lt;span class=&quot;param-name&quot;&gt;executionPhase:&lt;/span&gt; 실행 시점 (BEFORE_TEST_METHOD, AFTER_TEST_METHOD)&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Rollback&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 테스트 메서드 실행 후 트랜잭션 롤백 여부를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Test
@Rollback(false)
public void testCreateUser() {
    // 이 테스트는 롤백되지 않음
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@TestPropertySource&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 테스트용 프로퍼티 소스를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@TestPropertySource(
    locations = &quot;classpath:application-test.properties&quot;,
    properties = {
        &quot;spring.jpa.hibernate.ddl-auto=create-drop&quot;,
        &quot;logging.level.org.hibernate.SQL=DEBUG&quot;
    }
)
public class DatabaseTest { ... }&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;audit&quot;&gt;8. 감사(Auditing) 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@CreatedDate&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 생성된 날짜를 자동으로 설정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User {
    
    @CreatedDate
    private LocalDateTime createdAt;
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@LastModifiedDate&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 마지막으로 수정된 날짜를 자동으로 설정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@LastModifiedDate
private LocalDateTime updatedAt;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@CreatedBy&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티를 생성한 사용자를 자동으로 설정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@CreatedBy
private String createdBy;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@LastModifiedBy&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티를 마지막으로 수정한 사용자를 자동으로 설정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@LastModifiedBy
private String lastModifiedBy;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@EnableJpaAuditing&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; JPA Auditing 기능을 활성화합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Configuration
@EnableJpaAuditing
public class AuditConfig {
    
    @Bean
    public AuditorAware&amp;lt;String&amp;gt; auditorProvider() {
        return () -&amp;gt; {
            // 현재 사용자 정보 반환
            return Optional.of(&quot;system&quot;);
        };
    }
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@EntityListeners&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티 이벤트 리스너를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@EntityListeners(AuditingEntityListener.class)
public class User { ... }&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2 id=&quot;event&quot;&gt;9. 이벤트 관련 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PrePersist&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 영속화되기 전에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
public class User {
    
    @PrePersist
    public void prePersist() {
        this.createdAt = new Date();
        this.updatedAt = new Date();
    }
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PostPersist&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 영속화된 후에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PostPersist
public void postPersist() {
    log.info(&quot;새로운 사용자가 생성되었습니다: {}&quot;, this.id);
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PreUpdate&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 업데이트되기 전에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PreUpdate
public void preUpdate() {
    this.updatedAt = new Date();
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PostUpdate&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 업데이트된 후에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PostUpdate
public void postUpdate() {
    log.info(&quot;사용자 정보가 업데이트되었습니다: {}&quot;, this.id);
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PreRemove&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 삭제되기 전에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PreRemove
public void preRemove() {
    log.warn(&quot;사용자가 삭제됩니다: {}&quot;, this.id);
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PostRemove&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 삭제된 후에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PostRemove
public void postRemove() {
    log.info(&quot;사용자가 삭제되었습니다: {}&quot;, this.id);
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@PostLoad&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티가 데이터베이스에서 로드된 후에 실행됩니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@PostLoad
public void postLoad() {
    // 엔티티 로드 후 추가 초기화 작업
    this.displayName = this.firstName + &quot; &quot; + this.lastName;
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;h2&gt;10. 기타 유용한 어노테이션&lt;/h2&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@DynamicInsert / @DynamicUpdate&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; Hibernate가 동적으로 INSERT/UPDATE SQL을 생성하도록 합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@DynamicInsert
@DynamicUpdate
public class User {
    // null이 아닌 필드만 INSERT/UPDATE에 포함
}&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                성능 최적화에 도움이 되지만 SQL 캐싱 효과가 감소할 수 있습니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Where&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 엔티티 조회 시 추가 조건을 자동으로 적용합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Entity
@Where(clause = &quot;deleted = false&quot;)
public class User {
    
    @Column(name = &quot;deleted&quot;)
    private Boolean deleted = false;
}&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Formula&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 계산된 필드를 정의합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Formula(&quot;(SELECT COUNT(*) FROM orders o WHERE o.user_id = id)&quot;)
private int orderCount;&lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;annotation&quot;&gt;
            &lt;div class=&quot;annotation-name&quot;&gt;@Version&lt;/div&gt;
            &lt;div class=&quot;purpose&quot;&gt;
                &lt;strong&gt;목적:&lt;/strong&gt; 낙관적 락을 위한 버전 필드를 지정합니다.
            &lt;/div&gt;
            &lt;div class=&quot;code-block&quot;&gt;
@Version
private Long version;&lt;/div&gt;
            &lt;div class=&quot;info&quot;&gt;
                동시성 제어를 위해 사용되며, 엔티티 수정 시 자동으로 증가합니다.
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class=&quot;warning&quot;&gt;
            &lt;strong&gt;주의사항:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;li&gt;어노테이션을 과도하게 사용하면 코드 복잡성이 증가할 수 있습니다&lt;/li&gt;
                &lt;li&gt;성능 관련 어노테이션은 실제 측정을 통해 효과를 검증해야 합니다&lt;/li&gt;
                &lt;li&gt;데이터베이스 독립성을 고려하여 어노테이션을 선택해야 합니다&lt;/li&gt;
                &lt;li&gt;테스트 환경과 운영 환경의 설정이 다를 수 있으므로 주의가 필요합니다&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/div&gt;

        &lt;div class=&quot;info&quot;&gt;
            &lt;strong&gt;결론:&lt;/strong&gt; Spring과 JPA의 데이터베이스 관련 어노테이션들은 강력하고 편리한 기능을 제공합니다. 각 어노테이션의 특성과 용도를 정확히 이해하고 적절히 활용하면 효율적이고 안정적인 데이터 액세스 계층을 구축할 수 있습니다. 특히 성능 최적화와 관련된 어노테이션들은 실제 운영 환경에서의 성능 측정을 통해 효과를 검증하는 것이 중요합니다.
        &lt;/div&gt;

        &lt;div class=&quot;author&quot;&gt;
            &lt;p&gt;&lt;em&gt;이 가이드가 Spring/JPA 개발에 도움이 되었기를 바랍니다.&lt;/em&gt;&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/136</guid>
      <comments>https://devsite.tistory.com/entry/SpringJPA-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EA%B4%80%EB%A0%A8-%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry136comment</comments>
      <pubDate>Mon, 14 Jul 2025 12:50:56 +0900</pubDate>
    </item>
    <item>
      <title>Spring @Transactional readOnly 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Spring-Transactional-readOnly-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    
    &lt;style&gt;
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            line-height: 1.6;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f8f9fa;
            color: #333;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #2c3e50;
            border-bottom: 3px solid #3498db;
            padding-bottom: 10px;
            margin-bottom: 30px;
        }
        h2 {
            color: #34495e;
            margin-top: 30px;
            margin-bottom: 15px;
            border-left: 4px solid #3498db;
            padding-left: 15px;
        }
        h3 {
            color: #7f8c8d;
            margin-top: 25px;
            margin-bottom: 10px;
        }
        .code-block {
            background: #2c3e50;
            color: #ecf0f1;
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
            margin: 15px 0;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 14px;
        }
        .highlight {
            background: #fff3cd;
            padding: 15px;
            border-left: 4px solid #ffc107;
            margin: 15px 0;
        }
        .warning {
            background: #f8d7da;
            padding: 15px;
            border-left: 4px solid #dc3545;
            margin: 15px 0;
        }
        .info {
            background: #d1ecf1;
            padding: 15px;
            border-left: 4px solid #17a2b8;
            margin: 15px 0;
        }
        ul {
            padding-left: 20px;
        }
        li {
            margin-bottom: 8px;
        }
        .author {
            text-align: center;
            margin-top: 40px;
            padding-top: 20px;
            border-top: 1px solid #eee;
            color: #7f8c8d;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class=&quot;container&quot;&gt;
        &lt;h1&gt;Spring @Transactional readOnly 완벽 가이드&lt;/h1&gt;
        
        &lt;p&gt;Spring Framework에서 트랜잭션 관리는 애플리케이션 성능과 데이터 일관성에 큰 영향을 미치는 중요한 요소입니다. 그 중에서도 &lt;code&gt;@Transactional&lt;/code&gt; 어노테이션의 &lt;code&gt;readOnly&lt;/code&gt; 속성은 읽기 전용 트랜잭션을 통해 성능 최적화를 달성할 수 있는 강력한 도구입니다.&lt;/p&gt;

        &lt;h2&gt;1. @Transactional readOnly란?&lt;/h2&gt;
        
        &lt;p&gt;&lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;는 해당 메서드나 클래스가 읽기 전용 트랜잭션에서 실행되어야 함을 Spring에게 알려주는 어노테이션입니다. 이는 단순히 개발자에게 힌트를 제공하는 것이 아니라, 실제로 JPA/Hibernate와 데이터베이스 레벨에서 다양한 최적화를 수행합니다.&lt;/p&gt;

        &lt;div class=&quot;code-block&quot;&gt;
@Service
@Transactional(readOnly = true)
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    // 읽기 전용 메서드
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    // 쓰기 작업이 필요한 메서드는 readOnly를 false로 오버라이드
    @Transactional(readOnly = false)
    public User createUser(User user) {
        return userRepository.save(user);
    }
}&lt;/div&gt;

        &lt;h2&gt;2. readOnly가 제공하는 최적화 효과&lt;/h2&gt;

        &lt;h3&gt;2.1 Hibernate 레벨 최적화&lt;/h3&gt;
        
        &lt;div class=&quot;info&quot;&gt;
            &lt;strong&gt;FlushMode 변경:&lt;/strong&gt; Hibernate는 readOnly 트랜잭션에서 FlushMode를 MANUAL로 설정하여 자동 플러시를 비활성화합니다.
        &lt;/div&gt;

        &lt;p&gt;일반적으로 Hibernate는 트랜잭션 커밋 시점이나 쿼리 실행 전에 영속성 컨텍스트의 변경사항을 데이터베이스에 동기화(flush)합니다. 하지만 readOnly 트랜잭션에서는 이러한 플러시 작업이 불필요하므로 생략됩니다.&lt;/p&gt;

        &lt;div class=&quot;code-block&quot;&gt;
@Transactional(readOnly = true)
public List&amp;lt;User&amp;gt; findActiveUsers() {
    // 이 메서드에서는 flush 작업이 수행되지 않음
    return userRepository.findByActiveTrue();
}&lt;/div&gt;

        &lt;h3&gt;2.2 더티 체킹(Dirty Checking) 비활성화&lt;/h3&gt;
        
        &lt;p&gt;Hibernate는 영속성 컨텍스트에서 관리되는 엔티티의 변경사항을 추적하는 더티 체킹을 수행합니다. readOnly 트랜잭션에서는 이 기능이 비활성화되어 메모리 사용량과 CPU 사용량이 감소합니다.&lt;/p&gt;

        &lt;div class=&quot;code-block&quot;&gt;
@Transactional(readOnly = true)
public void processUsers() {
    List&amp;lt;User&amp;gt; users = userRepository.findAll();
    
    for (User user : users) {
        // 엔티티 변경이 있어도 더티 체킹이 수행되지 않음
        user.setLastAccessTime(new Date());
        // 실제로는 데이터베이스에 반영되지 않음
    }
}&lt;/div&gt;

        &lt;h3&gt;2.3 데이터베이스 레벨 최적화&lt;/h3&gt;
        
        &lt;p&gt;많은 데이터베이스 시스템에서는 읽기 전용 트랜잭션에 대해 특별한 최적화를 제공합니다:&lt;/p&gt;
        
        &lt;ul&gt;
            &lt;li&gt;&lt;strong&gt;락 최적화:&lt;/strong&gt; 읽기 전용 트랜잭션에서는 불필요한 락 획득을 피할 수 있습니다&lt;/li&gt;
            &lt;li&gt;&lt;strong&gt;커넥션 풀 최적화:&lt;/strong&gt; 읽기 전용 커넥션 풀을 별도로 관리하여 성능을 향상시킬 수 있습니다&lt;/li&gt;
            &lt;li&gt;&lt;strong&gt;복제본 라우팅:&lt;/strong&gt; 마스터-슬레이브 구조에서 읽기 쿼리를 슬레이브 노드로 라우팅할 수 있습니다&lt;/li&gt;
        &lt;/ul&gt;

        &lt;h2&gt;3. 실제 사용 예제&lt;/h2&gt;

        &lt;h3&gt;3.1 Repository 레이어에서의 활용&lt;/h3&gt;
        
        &lt;div class=&quot;code-block&quot;&gt;
@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    
    // 메서드 레벨에서 readOnly 설정
    @Query(&quot;SELECT u FROM User u WHERE u.email = :email&quot;)
    @Transactional(readOnly = true)
    Optional&amp;lt;User&amp;gt; findByEmail(@Param(&quot;email&quot;) String email);
    
    // 복잡한 조회 쿼리
    @Query(&quot;SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.active = true&quot;)
    @Transactional(readOnly = true)
    List&amp;lt;User&amp;gt; findActiveUsersWithOrders();
}&lt;/div&gt;

        &lt;h3&gt;3.2 Service 레이어에서의 활용&lt;/h3&gt;
        
        &lt;div class=&quot;code-block&quot;&gt;
@Service
public class OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional(readOnly = true)
    public OrderSummary getOrderSummary(Long userId) {
        List&amp;lt;Order&amp;gt; orders = orderRepository.findByUserId(userId);
        
        return OrderSummary.builder()
                .totalOrders(orders.size())
                .totalAmount(orders.stream()
                        .mapToDouble(Order::getAmount)
                        .sum())
                .build();
    }
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setAmount(request.getAmount());
        
        return orderRepository.save(order);
    }
}&lt;/div&gt;

        &lt;h2&gt;4. 주의사항과 함정&lt;/h2&gt;

        &lt;div class=&quot;warning&quot;&gt;
            &lt;strong&gt;주의!&lt;/strong&gt; readOnly 트랜잭션에서 데이터 변경 작업을 수행하면 예상과 다른 결과가 발생할 수 있습니다.
        &lt;/div&gt;

        &lt;h3&gt;4.1 데이터 변경 작업이 무시되는 경우&lt;/h3&gt;
        
        &lt;div class=&quot;code-block&quot;&gt;
@Transactional(readOnly = true)
public void updateUserName(Long userId, String newName) {
    User user = userRepository.findById(userId).orElseThrow();
    user.setName(newName); // 변경은 되지만
    userRepository.save(user); // 실제로는 데이터베이스에 저장되지 않음
}&lt;/div&gt;

        &lt;p&gt;위 코드는 예외를 발생시키지 않지만, 실제로는 데이터베이스에 변경사항이 반영되지 않습니다. 이는 디버깅하기 어려운 버그의 원인이 될 수 있습니다.&lt;/p&gt;

        &lt;h3&gt;4.2 트랜잭션 전파와의 상호작용&lt;/h3&gt;
        
        &lt;div class=&quot;code-block&quot;&gt;
@Service
public class UserService {
    
    @Transactional(readOnly = true)
    public void processUser(Long userId) {
        User user = userRepository.findById(userId).orElseThrow();
        
        // 이 메서드는 새로운 트랜잭션을 시작하므로 정상 동작
        updateUserStatus(user);
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateUserStatus(User user) {
        user.setStatus(UserStatus.PROCESSED);
        userRepository.save(user);
    }
}&lt;/div&gt;

        &lt;h2&gt;5. 성능 측정 및 모니터링&lt;/h2&gt;

        &lt;p&gt;readOnly 트랜잭션의 효과를 측정하려면 다음과 같은 방법을 사용할 수 있습니다:&lt;/p&gt;

        &lt;div class=&quot;code-block&quot;&gt;
@Component
@Slf4j
public class TransactionMonitor {
    
    @EventListener
    public void handleTransactionEvent(TransactionEvent event) {
        if (event.isReadOnly()) {
            log.info(&quot;ReadOnly transaction completed: {} ms&quot;, 
                    event.getExecutionTime());
        }
    }
}&lt;/div&gt;

        &lt;h2&gt;6. 베스트 프랙티스&lt;/h2&gt;

        &lt;div class=&quot;highlight&quot;&gt;
            &lt;strong&gt;권장사항:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;li&gt;Service 클래스 레벨에서 readOnly = true로 설정하고, 쓰기 작업이 필요한 메서드에서만 오버라이드&lt;/li&gt;
                &lt;li&gt;조회 전용 Repository 메서드에는 명시적으로 readOnly = true 설정&lt;/li&gt;
                &lt;li&gt;복잡한 보고서 생성이나 통계 조회 메서드에는 반드시 readOnly 사용&lt;/li&gt;
                &lt;li&gt;읽기 전용 트랜잭션에서는 엔티티 변경 작업을 피하고, 필요시 별도 메서드로 분리&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/div&gt;

        &lt;h2&gt;7. 결론&lt;/h2&gt;

        &lt;p&gt;Spring의 &lt;code&gt;@Transactional(readOnly = true)&lt;/code&gt;는 단순한 힌트가 아닌 실제 성능 최적화를 제공하는 강력한 도구입니다. 올바르게 사용하면 메모리 사용량 감소, CPU 사용량 감소, 그리고 데이터베이스 레벨에서의 최적화 효과를 얻을 수 있습니다.&lt;/p&gt;

        &lt;p&gt;하지만 읽기 전용 트랜잭션에서 데이터 변경 작업을 수행할 때의 함정을 이해하고, 적절한 트랜잭션 전파 설정을 통해 예상치 못한 동작을 방지하는 것이 중요합니다. 성능 최적화와 함께 코드의 명확성과 유지보수성을 고려하여 readOnly 속성을 적절히 활용하시기 바랍니다.&lt;/p&gt;

        &lt;div class=&quot;author&quot;&gt;
            &lt;p&gt;&lt;em&gt;이 글이 Spring 트랜잭션 관리에 대한 이해에 도움이 되었기를 바랍니다.&lt;/em&gt;&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/135</guid>
      <comments>https://devsite.tistory.com/entry/Spring-Transactional-readOnly-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry135comment</comments>
      <pubDate>Mon, 14 Jul 2025 12:44:42 +0900</pubDate>
    </item>
    <item>
      <title>[CursorAI]  점심은 뭘 먹을까? 점심룰렛.</title>
      <link>https://devsite.tistory.com/entry/CursorAI-%EC%A0%90%EC%8B%AC%EC%9D%80-%EB%AD%98%EB%A8%B9%EC%9D%84%EA%B9%8C-%EC%A0%90%EC%8B%AC%EB%A3%B0%EB%A0%9B</link>
      <description>&lt;div style=&quot;text-align: center; font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px;&quot;&gt;
&lt;h2 style=&quot;color: #333;&quot; data-ke-size=&quot;size26&quot;&gt;  점심 룰렛&lt;/h2&gt;
&lt;!-- 룰렛 결과 표시 영역 --&gt;
&lt;div id=&quot;result&quot; style=&quot;margin: 20px 0; padding: 20px; border: 2px solid #4CAF50; border-radius: 10px; font-size: 18px; font-weight: bold; min-height: 30px;&quot;&gt;결과가 여기에 표시됩니다&lt;/div&gt;
&lt;!-- 돌리기 버튼 --&gt; &lt;button style=&quot;background-color: #4caf50; color: white; border: none; padding: 10px 20px; font-size: 16px; border-radius: 5px; cursor: pointer;&quot;&gt;돌리기&lt;/button&gt; &lt;!-- 음식점 목록 관리 --&gt;
&lt;div style=&quot;margin-top: 30px; border-top: 1px solid #ddd; padding-top: 20px;&quot;&gt;
&lt;div style=&quot;margin-bottom: 10px; font-weight: bold;&quot;&gt;음식점 목록&lt;/div&gt;
&lt;div id=&quot;restaurantList&quot; style=&quot;margin-bottom: 10px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;input id=&quot;newRestaurant&quot; style=&quot;padding: 5px; margin-right: 5px; width: 200px;&quot; type=&quot;text&quot; placeholder=&quot;새로운 음식점 이름&quot; /&gt; &lt;button style=&quot;background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;&quot;&gt; 추가 &lt;/button&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;
// 기본 음식점 목록
let restaurants = [
    '김밥천국', '맥도날드', '한솥도시락', '서브웨이',
    '본죽', '파리바게뜨', '롯데리아', '피자헛'
];

// 음식점 목록 표시 함수
function displayRestaurants() {
    const list = document.getElementById('restaurantList');
    list.innerHTML = '';
    
    restaurants.forEach((restaurant, index) =&gt; {
        const item = document.createElement('div');
        item.style.margin = '5px 0';
        item.innerHTML = `
            &lt;span style=&quot;margin-right: 10px;&quot;&gt;${restaurant}&lt;/span&gt;
            &lt;button onclick=&quot;deleteRestaurant(${index})&quot; 
                    style=&quot;background-color: #dc3545; color: white; border: none; padding: 2px 5px; border-radius: 3px; cursor: pointer; font-size: 12px;&quot;&gt;
                삭제
            &lt;/button&gt;
        `;
        list.appendChild(item);
    });
}

// 룰렛 돌리기 함수
function spinRoulette() {
    if (restaurants.length &lt; 2) {
        alert('최소 2개의 음식점이 필요합니다!');
        return;
    }
    
    // 랜덤하게 음식점 선택
    const randomIndex = Math.floor(Math.random() * restaurants.length);
    const selected = restaurants[randomIndex];
    
    // 결과 표시
    const result = document.getElementById('result');
    result.innerHTML = `  오늘의 점심: &lt;span style=&quot;color: #4CAF50;&quot;&gt;${selected}&lt;/span&gt;`;
}

// 음식점 추가 함수
function addRestaurant() {
    const input = document.getElementById('newRestaurant');
    const newName = input.value.trim();
    
    if (newName) {
        restaurants.push(newName);
        input.value = '';
        displayRestaurants();
    } else {
        alert('음식점 이름을 입력해주세요!');
    }
}

// 음식점 삭제 함수
function deleteRestaurant(index) {
    if (restaurants.length &gt; 2) {
        restaurants.splice(index, 1);
        displayRestaurants();
    } else {
        alert('최소 2개의 음식점이 필요합니다!');
    }
}

// 초기 음식점 목록 표시
displayRestaurants();
&lt;/script&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1748527562014&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div style=&quot;text-align: center; font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px;&quot;&amp;gt;
    &amp;lt;h2 style=&quot;color: #333;&quot;&amp;gt;  점심 룰렛&amp;lt;/h2&amp;gt;
    
    &amp;lt;!-- 룰렛 결과 표시 영역 --&amp;gt;
    &amp;lt;div id=&quot;result&quot; style=&quot;margin: 20px 0; padding: 20px; border: 2px solid #4CAF50; border-radius: 10px; font-size: 18px; font-weight: bold; min-height: 30px;&quot;&amp;gt;
        결과가 여기에 표시됩니다
    &amp;lt;/div&amp;gt;
    
    &amp;lt;!-- 돌리기 버튼 --&amp;gt;
    &amp;lt;button onclick=&quot;spinRoulette()&quot; style=&quot;background-color: #4CAF50; color: white; border: none; padding: 10px 20px; font-size: 16px; border-radius: 5px; cursor: pointer;&quot;&amp;gt;돌리기&amp;lt;/button&amp;gt;
    
    &amp;lt;!-- 음식점 목록 관리 --&amp;gt;
    &amp;lt;div style=&quot;margin-top: 30px; border-top: 1px solid #ddd; padding-top: 20px;&quot;&amp;gt;
        &amp;lt;div style=&quot;margin-bottom: 10px; font-weight: bold;&quot;&amp;gt;음식점 목록&amp;lt;/div&amp;gt;
        &amp;lt;div id=&quot;restaurantList&quot; style=&quot;margin-bottom: 10px;&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;input type=&quot;text&quot; id=&quot;newRestaurant&quot; placeholder=&quot;새로운 음식점 이름&quot; 
               style=&quot;padding: 5px; margin-right: 5px; width: 200px;&quot;&amp;gt;
        &amp;lt;button onclick=&quot;addRestaurant()&quot; 
                style=&quot;background-color: #007bff; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;&quot;&amp;gt;
            추가
        &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
// 기본 음식점 목록
let restaurants = [
    '김밥천국', '맥도날드', '한솥도시락', '서브웨이',
    '본죽', '파리바게뜨', '롯데리아', '피자헛'
];

// 음식점 목록 표시 함수
function displayRestaurants() {
    const list = document.getElementById('restaurantList');
    list.innerHTML = '';
    
    restaurants.forEach((restaurant, index) =&amp;gt; {
        const item = document.createElement('div');
        item.style.margin = '5px 0';
        item.innerHTML = `
            &amp;lt;span style=&quot;margin-right: 10px;&quot;&amp;gt;${restaurant}&amp;lt;/span&amp;gt;
            &amp;lt;button onclick=&quot;deleteRestaurant(${index})&quot; 
                    style=&quot;background-color: #dc3545; color: white; border: none; padding: 2px 5px; border-radius: 3px; cursor: pointer; font-size: 12px;&quot;&amp;gt;
                삭제
            &amp;lt;/button&amp;gt;
        `;
        list.appendChild(item);
    });
}

// 룰렛 돌리기 함수
function spinRoulette() {
    if (restaurants.length &amp;lt; 2) {
        alert('최소 2개의 음식점이 필요합니다!');
        return;
    }
    
    // 랜덤하게 음식점 선택
    const randomIndex = Math.floor(Math.random() * restaurants.length);
    const selected = restaurants[randomIndex];
    
    // 결과 표시
    const result = document.getElementById('result');
    result.innerHTML = `  오늘의 점심: &amp;lt;span style=&quot;color: #4CAF50;&quot;&amp;gt;${selected}&amp;lt;/span&amp;gt;`;
}

// 음식점 추가 함수
function addRestaurant() {
    const input = document.getElementById('newRestaurant');
    const newName = input.value.trim();
    
    if (newName) {
        restaurants.push(newName);
        input.value = '';
        displayRestaurants();
    } else {
        alert('음식점 이름을 입력해주세요!');
    }
}

// 음식점 삭제 함수
function deleteRestaurant(index) {
    if (restaurants.length &amp;gt; 2) {
        restaurants.splice(index, 1);
        displayRestaurants();
    } else {
        alert('최소 2개의 음식점이 필요합니다!');
    }
}

// 초기 음식점 목록 표시
displayRestaurants();
&amp;lt;/script&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <category>Ai</category>
      <category>cursorai 예제</category>
      <category>룰렛</category>
      <category>점심</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/134</guid>
      <comments>https://devsite.tistory.com/entry/CursorAI-%EC%A0%90%EC%8B%AC%EC%9D%80-%EB%AD%98%EB%A8%B9%EC%9D%84%EA%B9%8C-%EC%A0%90%EC%8B%AC%EB%A3%B0%EB%A0%9B#entry134comment</comments>
      <pubDate>Thu, 29 May 2025 23:00:20 +0900</pubDate>
    </item>
    <item>
      <title>Oracle, MySQL, MSSQL 날짜 함수 완벽 비교 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-MySQL-MSSQL-%EB%82%A0%EC%A7%9C-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EB%B9%84%EA%B5%90-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    
    &lt;style&gt;
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            margin: 0;
            padding: 0;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: #333;
            line-height: 1.6;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            box-shadow: 0 0 30px rgba(0,0,0,0.2);
        }
        
        .header {
            background: linear-gradient(45deg, #2c3e50, #3498db);
            color: white;
            padding: 40px;
            text-align: center;
            position: relative;
            overflow: hidden;
        }
        
        .header::before {
            content: '';
            position: absolute;
            top: -50%;
            left: -50%;
            width: 200%;
            height: 200%;
            background: repeating-linear-gradient(
                45deg,
                transparent,
                transparent 2px,
                rgba(255,255,255,0.1) 2px,
                rgba(255,255,255,0.1) 4px
            );
            animation: move 20s linear infinite;
        }
        
        @keyframes move {
            0% { transform: translate(-50%, -50%) rotate(0deg); }
            100% { transform: translate(-50%, -50%) rotate(360deg); }
        }
        
        .header h1 {
            margin: 0;
            font-size: 2.5em;
            position: relative;
            z-index: 1;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
        }
        
        .content {
            padding: 40px;
        }
        
        .intro {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            padding: 30px;
            border-radius: 15px;
            margin-bottom: 30px;
            border-left: 5px solid #3498db;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
        }
        
        .db-tabs {
            display: flex;
            background: #f8f9fa;
            border-radius: 10px;
            padding: 5px;
            margin-bottom: 30px;
            box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .db-tab {
            flex: 1;
            text-align: center;
            padding: 15px;
            cursor: pointer;
            border-radius: 8px;
            transition: all 0.3s ease;
            font-weight: bold;
        }
        
        .db-tab.oracle {
            background: linear-gradient(135deg, #ff6b6b, #ee5a24);
            color: white;
        }
        
        .db-tab.mysql {
            background: linear-gradient(135deg, #4834d4, #686de0);
            color: white;
        }
        
        .db-tab.mssql {
            background: linear-gradient(135deg, #00d2d3, #54a0ff);
            color: white;
        }
        
        .db-tab:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(0,0,0,0.2);
        }
        
        .comparison-table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
        }
        
        .comparison-table th {
            background: linear-gradient(135deg, #2c3e50, #3498db);
            color: white;
            padding: 15px;
            text-align: left;
            font-weight: bold;
        }
        
        .comparison-table td {
            padding: 15px;
            border-bottom: 1px solid #eee;
            transition: background-color 0.3s ease;
        }
        
        .comparison-table tr:hover td {
            background-color: #f8f9fa;
        }
        
        .oracle-col {
            background: linear-gradient(135deg, #ffe8e8, #ffcdd2);
        }
        
        .mysql-col {
            background: linear-gradient(135deg, #e8e8ff, #c5cae9);
        }
        
        .mssql-col {
            background: linear-gradient(135deg, #e8ffff, #b2ebf2);
        }
        
        .code-block {
            background: #2d3748;
            color: #e2e8f0;
            padding: 20px;
            border-radius: 10px;
            margin: 15px 0;
            overflow-x: auto;
            position: relative;
            border-left: 4px solid #4299e1;
        }
        
        .code-block::before {
            content: attr(data-lang);
            position: absolute;
            top: 10px;
            right: 15px;
            background: #4299e1;
            color: white;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 0.8em;
            font-weight: bold;
        }
        
        .section {
            margin: 40px 0;
            padding: 30px;
            background: white;
            border-radius: 15px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.05);
            border: 1px solid #e1e5e9;
        }
        
        .section h2 {
            color: #2c3e50;
            border-bottom: 3px solid #3498db;
            padding-bottom: 10px;
            margin-bottom: 20px;
            position: relative;
        }
        
        .section h2::after {
            content: '';
            position: absolute;
            bottom: -3px;
            left: 0;
            width: 50px;
            height: 3px;
            background: #e74c3c;
        }
        
        .highlight-box {
            background: linear-gradient(135deg, #fff3cd, #ffeaa7);
            border: 1px solid #ffeaa7;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            border-left: 5px solid #f39c12;
        }
        
        .tip-box {
            background: linear-gradient(135deg, #d1ecf1, #b8daff);
            border: 1px solid #b8daff;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            border-left: 5px solid #007bff;
        }
        
        .tip-box::before {
            content: &quot;  팁&quot;;
            font-weight: bold;
            color: #007bff;
            display: block;
            margin-bottom: 10px;
        }
        
        .warning-box {
            background: linear-gradient(135deg, #f8d7da, #f5c6cb);
            border: 1px solid #f5c6cb;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            border-left: 5px solid #dc3545;
        }
        
        .warning-box::before {
            content: &quot;⚠️ 주의&quot;;
            font-weight: bold;
            color: #dc3545;
            display: block;
            margin-bottom: 10px;
        }
        
        .feature-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin: 30px 0;
        }
        
        .feature-card {
            background: white;
            border-radius: 15px;
            padding: 25px;
            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
            border: 1px solid #e1e5e9;
            transition: all 0.3s ease;
        }
        
        .feature-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 10px 25px rgba(0,0,0,0.15);
        }
        
        .feature-card h3 {
            color: #2c3e50;
            margin-top: 0;
            display: flex;
            align-items: center;
        }
        
        .icon {
            width: 24px;
            height: 24px;
            margin-right: 10px;
            border-radius: 50%;
            display: inline-block;
        }
        
        .oracle-icon { background: linear-gradient(135deg, #ff6b6b, #ee5a24); }
        .mysql-icon { background: linear-gradient(135deg, #4834d4, #686de0); }
        .mssql-icon { background: linear-gradient(135deg, #00d2d3, #54a0ff); }
        
        .footer {
            background: linear-gradient(45deg, #2c3e50, #3498db);
            color: white;
            padding: 30px;
            text-align: center;
        }
        
        @media (max-width: 768px) {
            .header h1 {
                font-size: 1.8em;
            }
            
            .content {
                padding: 20px;
            }
            
            .db-tabs {
                flex-direction: column;
            }
            
            .feature-grid {
                grid-template-columns: 1fr;
            }
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class=&quot;container&quot;&gt;
        
        &lt;div class=&quot;content&quot;&gt;
            &lt;div class=&quot;intro&quot;&gt;
                &lt;p&gt;데이터베이스에서 날짜와 시간 처리는 매우 중요한 기능 중 하나입니다. 하지만 Oracle, MySQL, MS SQL Server는 각각 다른 날짜 함수와 문법을 제공하여, 개발자들이 다른 데이터베이스로 마이그레이션할 때 가장 혼란스러워하는 부분 중 하나입니다. 각 데이터베이스 시스템의 날짜 함수 차이점과 대체 방법을 상세히 비교해보겠습니다.&lt;/p&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;db-tabs&quot;&gt;
                &lt;div class=&quot;db-tab oracle&quot;&gt;
                    &lt;span class=&quot;icon oracle-icon&quot;&gt;&lt;/span&gt;Oracle
                &lt;/div&gt;
                &lt;div class=&quot;db-tab mysql&quot;&gt;
                    &lt;span class=&quot;icon mysql-icon&quot;&gt;&lt;/span&gt;MySQL
                &lt;/div&gt;
                &lt;div class=&quot;db-tab mssql&quot;&gt;
                    &lt;span class=&quot;icon mssql-icon&quot;&gt;&lt;/span&gt;MS SQL Server
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  1. 현재 날짜/시간 구하기&lt;/h2&gt;
                
                &lt;p&gt;가장 기본적인 현재 날짜와 시간을 구하는 함수부터 각 데이터베이스마다 다릅니다.&lt;/p&gt;
                
                &lt;table class=&quot;comparison-table&quot;&gt;
                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;기능&lt;/th&gt;
                            &lt;th class=&quot;oracle-col&quot;&gt;Oracle&lt;/th&gt;
                            &lt;th class=&quot;mysql-col&quot;&gt;MySQL&lt;/th&gt;
                            &lt;th class=&quot;mssql-col&quot;&gt;MS SQL Server&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;현재 날짜+시간&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;SYSDATE&lt;br/&gt;SYSTIMESTAMP&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;NOW()&lt;br/&gt;CURRENT_TIMESTAMP&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;GETDATE()&lt;br/&gt;CURRENT_TIMESTAMP&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;현재 날짜만&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TRUNC(SYSDATE)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;CURDATE()&lt;br/&gt;CURRENT_DATE&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;CAST(GETDATE() AS DATE)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;현재 시간만&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TO_CHAR(SYSDATE, 'HH24:MI:SS')&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;CURTIME()&lt;br/&gt;CURRENT_TIME&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;CAST(GETDATE() AS TIME)&lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon oracle-icon&quot;&gt;&lt;/span&gt;Oracle 예제&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;Oracle&quot;&gt;
-- 현재 날짜/시간
SELECT SYSDATE FROM dual;
-- 결과: 2025-01-15 14:30:25

-- 타임스탬프 (밀리초 포함)
SELECT SYSTIMESTAMP FROM dual;
-- 결과: 2025-01-15 14:30:25.123456 +09:00

-- 현재 날짜만
SELECT TRUNC(SYSDATE) FROM dual;
-- 결과: 2025-01-15 00:00:00
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mysql-icon&quot;&gt;&lt;/span&gt;MySQL 예제&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MySQL&quot;&gt;
-- 현재 날짜/시간
SELECT NOW();
-- 결과: 2025-01-15 14:30:25

-- 현재 날짜만
SELECT CURDATE();
-- 결과: 2025-01-15

-- 현재 시간만
SELECT CURTIME();
-- 결과: 14:30:25
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mssql-icon&quot;&gt;&lt;/span&gt;MS SQL Server 예제&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MSSQL&quot;&gt;
-- 현재 날짜/시간
SELECT GETDATE();
-- 결과: 2025-01-15 14:30:25.123

-- 현재 날짜만
SELECT CAST(GETDATE() AS DATE);
-- 결과: 2025-01-15

-- 현재 시간만
SELECT CAST(GETDATE() AS TIME);
-- 결과: 14:30:25.1230000
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;➕ 2. 날짜 연산 (더하기/빼기)&lt;/h2&gt;
                
                &lt;p&gt;날짜에 일, 월, 년을 더하거나 빼는 방법도 각 데이터베이스마다 완전히 다릅니다.&lt;/p&gt;
                
                &lt;table class=&quot;comparison-table&quot;&gt;
                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;연산&lt;/th&gt;
                            &lt;th class=&quot;oracle-col&quot;&gt;Oracle&lt;/th&gt;
                            &lt;th class=&quot;mysql-col&quot;&gt;MySQL&lt;/th&gt;
                            &lt;th class=&quot;mssql-col&quot;&gt;MS SQL Server&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;일 더하기&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;SYSDATE + 7&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_ADD(NOW(), INTERVAL 7 DAY)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEADD(day, 7, GETDATE())&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;월 더하기&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;ADD_MONTHS(SYSDATE, 3)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_ADD(NOW(), INTERVAL 3 MONTH)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEADD(month, 3, GETDATE())&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;년 더하기&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;ADD_MONTHS(SYSDATE, 12)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_ADD(NOW(), INTERVAL 1 YEAR)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEADD(year, 1, GETDATE())&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;일 빼기&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;SYSDATE - 30&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_SUB(NOW(), INTERVAL 30 DAY)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEADD(day, -30, GETDATE())&lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                
                &lt;div class=&quot;highlight-box&quot;&gt;
                    &lt;h4&gt;  실무 예제: 최근 30일간 주문 데이터 조회&lt;/h4&gt;
                &lt;/div&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon oracle-icon&quot;&gt;&lt;/span&gt;Oracle&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;Oracle&quot;&gt;
SELECT order_id, customer_id, order_date, total_amount
FROM orders
WHERE order_date &gt;= SYSDATE - 30
  AND order_date &lt; SYSDATE + 1
ORDER BY order_date DESC;

-- 월별 조회 (지난 3개월)
SELECT order_id, customer_id, order_date
FROM orders
WHERE order_date &gt;= ADD_MONTHS(SYSDATE, -3)
ORDER BY order_date DESC;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mysql-icon&quot;&gt;&lt;/span&gt;MySQL&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MySQL&quot;&gt;
SELECT order_id, customer_id, order_date, total_amount
FROM orders
WHERE order_date &gt;= DATE_SUB(NOW(), INTERVAL 30 DAY)
  AND order_date &lt;= NOW()
ORDER BY order_date DESC;

-- 월별 조회 (지난 3개월)
SELECT order_id, customer_id, order_date
FROM orders
WHERE order_date &gt;= DATE_SUB(NOW(), INTERVAL 3 MONTH)
ORDER BY order_date DESC;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mssql-icon&quot;&gt;&lt;/span&gt;MS SQL Server&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MSSQL&quot;&gt;
SELECT order_id, customer_id, order_date, total_amount
FROM orders
WHERE order_date &gt;= DATEADD(day, -30, GETDATE())
  AND order_date &lt;= GETDATE()
ORDER BY order_date DESC;

-- 월별 조회 (지난 3개월)
SELECT order_id, customer_id, order_date
FROM orders
WHERE order_date &gt;= DATEADD(month, -3, GETDATE())
ORDER BY order_date DESC;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  3. 날짜 차이 계산&lt;/h2&gt;
                
                &lt;p&gt;두 날짜 간의 차이를 계산하는 방법도 각각 다릅니다.&lt;/p&gt;
                
                &lt;table class=&quot;comparison-table&quot;&gt;
                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;차이 단위&lt;/th&gt;
                            &lt;th class=&quot;oracle-col&quot;&gt;Oracle&lt;/th&gt;
                            &lt;th class=&quot;mysql-col&quot;&gt;MySQL&lt;/th&gt;
                            &lt;th class=&quot;mssql-col&quot;&gt;MS SQL Server&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;일 차이&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;date2 - date1&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATEDIFF(date2, date1)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEDIFF(day, date1, date2)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;월 차이&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;MONTHS_BETWEEN(date2, date1)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;TIMESTAMPDIFF(MONTH, date1, date2)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEDIFF(month, date1, date2)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;년 차이&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;MONTHS_BETWEEN(date2, date1) / 12&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;TIMESTAMPDIFF(YEAR, date1, date2)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEDIFF(year, date1, date2)&lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                
                &lt;div class=&quot;tip-box&quot;&gt;
                    MySQL의 DATEDIFF는 일 차이만 계산하고, 시간/분/초 차이는 TIMESTAMPDIFF를 사용해야 합니다. 반면 MS SQL Server의 DATEDIFF는 다양한 단위를 지원합니다.
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  4. 날짜 포맷팅&lt;/h2&gt;
                
                &lt;p&gt;날짜를 원하는 형식의 문자열로 변환하는 방법도 각각 완전히 다릅니다.&lt;/p&gt;
                
                &lt;table class=&quot;comparison-table&quot;&gt;
                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;형식&lt;/th&gt;
                            &lt;th class=&quot;oracle-col&quot;&gt;Oracle&lt;/th&gt;
                            &lt;th class=&quot;mysql-col&quot;&gt;MySQL&lt;/th&gt;
                            &lt;th class=&quot;mssql-col&quot;&gt;MS SQL Server&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;YYYY-MM-DD&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TO_CHAR(date, 'YYYY-MM-DD')&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_FORMAT(date, '%Y-%m-%d')&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;FORMAT(date, 'yyyy-MM-dd')&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;DD/MM/YYYY&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TO_CHAR(date, 'DD/MM/YYYY')&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_FORMAT(date, '%d/%m/%Y')&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;FORMAT(date, 'dd/MM/yyyy')&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;한국어 형식&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TO_CHAR(date, 'YYYY&quot;년&quot; MM&quot;월&quot; DD&quot;일&quot;')&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DATE_FORMAT(date, '%Y년 %m월 %d일')&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;FORMAT(date, 'yyyy년 MM월 dd일')&lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon oracle-icon&quot;&gt;&lt;/span&gt;Oracle 포맷팅&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;Oracle&quot;&gt;
-- 다양한 형식 예제
SELECT 
  TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') AS datetime_format,
  TO_CHAR(SYSDATE, 'YYYY&quot;년&quot; MM&quot;월&quot; DD&quot;일&quot;') AS korean_format,
  TO_CHAR(SYSDATE, 'Day, DD Month YYYY') AS english_format,
  TO_CHAR(SYSDATE, 'Q') AS quarter,
  TO_CHAR(SYSDATE, 'WW') AS week_of_year
FROM dual;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mysql-icon&quot;&gt;&lt;/span&gt;MySQL 포맷팅&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MySQL&quot;&gt;
-- 다양한 형식 예제
SELECT 
  DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') AS datetime_format,
  DATE_FORMAT(NOW(), '%Y년 %m월 %d일') AS korean_format,
  DATE_FORMAT(NOW(), '%W, %d %M %Y') AS english_format,
  QUARTER(NOW()) AS quarter,
  WEEK(NOW()) AS week_of_year;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mssql-icon&quot;&gt;&lt;/span&gt;MS SQL Server 포맷팅&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MSSQL&quot;&gt;
-- 다양한 형식 예제
SELECT 
  FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss') AS datetime_format,
  FORMAT(GETDATE(), 'yyyy년 MM월 dd일') AS korean_format,
  FORMAT(GETDATE(), 'dddd, dd MMMM yyyy', 'en-US') AS english_format,
  DATEPART(QUARTER, GETDATE()) AS quarter,
  DATEPART(WEEK, GETDATE()) AS week_of_year;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  5. 날짜 추출 함수&lt;/h2&gt;
                
                &lt;p&gt;날짜에서 년, 월, 일 등의 특정 부분을 추출하는 방법입니다.&lt;/p&gt;
                
                &lt;table class=&quot;comparison-table&quot;&gt;
                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;추출 요소&lt;/th&gt;
                            &lt;th class=&quot;oracle-col&quot;&gt;Oracle&lt;/th&gt;
                            &lt;th class=&quot;mysql-col&quot;&gt;MySQL&lt;/th&gt;
                            &lt;th class=&quot;mssql-col&quot;&gt;MS SQL Server&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;년도&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;EXTRACT(YEAR FROM date)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;YEAR(date)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;YEAR(date)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;월&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;EXTRACT(MONTH FROM date)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;MONTH(date)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;MONTH(date)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;일&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;EXTRACT(DAY FROM date)&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DAY(date)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DAY(date)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;요일&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TO_CHAR(date, 'D')&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;DAYOFWEEK(date)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEPART(WEEKDAY, date)&lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;분기&lt;/strong&gt;&lt;/td&gt;
                            &lt;td class=&quot;oracle-col&quot;&gt;TO_CHAR(date, 'Q')&lt;/td&gt;
                            &lt;td class=&quot;mysql-col&quot;&gt;QUARTER(date)&lt;/td&gt;
                            &lt;td class=&quot;mssql-col&quot;&gt;DATEPART(QUARTER, date)&lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                
                &lt;div class=&quot;warning-box&quot;&gt;
                    요일 번호는 각 데이터베이스마다 다릅니다! Oracle과 MS SQL Server는 일요일이 1, MySQL은 일요일이 1이지만 설정에 따라 달라질 수 있습니다.
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  6. 특별한 날짜 함수들&lt;/h2&gt;
                
                &lt;p&gt;각 데이터베이스만의 독특한 날짜 관련 함수들을 살펴보겠습니다.&lt;/p&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon oracle-icon&quot;&gt;&lt;/span&gt;Oracle 특별 함수&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;Oracle&quot;&gt;
-- LAST_DAY: 해당 월의 마지막 날
SELECT LAST_DAY(SYSDATE) FROM dual;
-- 결과: 2025-01-31

-- NEXT_DAY: 다음 요일 찾기
SELECT NEXT_DAY(SYSDATE, '일요일') FROM dual;

-- TRUNC: 날짜 자르기
SELECT TRUNC(SYSDATE, 'MONTH') FROM dual; -- 월 첫날
SELECT TRUNC(SYSDATE, 'YEAR') FROM dual;  -- 년 첫날

-- ROUND: 날짜 반올림
SELECT ROUND(SYSDATE, 'MONTH') FROM dual;

-- ADD_MONTHS: 월 더하기 (말일 처리 자동)
SELECT ADD_MONTHS('2025-01-31', 1) FROM dual;
-- 결과: 2025-02-28 (2월 말일로 자동 조정)
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mysql-icon&quot;&gt;&lt;/span&gt;MySQL 특별 함수&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MySQL&quot;&gt;
-- LAST_DAY: 해당 월의 마지막 날
SELECT LAST_DAY(NOW());
-- 결과: 2025-01-31

-- DAYNAME: 요일명 반환
SELECT DAYNAME(NOW());
-- 결과: Wednesday

-- WEEKDAY: 요일 번호 (월요일=0)
SELECT WEEKDAY(NOW());

-- STR_TO_DATE: 문자열을 날짜로 변환
SELECT STR_TO_DATE('2025-01-15', '%Y-%m-%d');

-- MAKEDATE: 년도와 일수로 날짜 생성
SELECT MAKEDATE(2025, 100); -- 2025년의 100번째 날

-- PERIOD_DIFF: 기간 차이 (YYYYMM 형식)
SELECT PERIOD_DIFF(202501, 202412);
-- 결과: 1 (1개월 차이)
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mssql-icon&quot;&gt;&lt;/span&gt;MS SQL Server 특별 함수&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MSSQL&quot;&gt;
-- EOMONTH: 해당 월의 마지막 날
SELECT EOMONTH(GETDATE());
-- 결과: 2025-01-31

-- DATEFROMPARTS: 년,월,일로 날짜 생성
SELECT DATEFROMPARTS(2025, 12, 25);
-- 결과: 2025-12-25

-- TIMEFROMPARTS: 시,분,초로 시간 생성
SELECT TIMEFROMPARTS(14, 30, 0, 0, 0);

-- ISDATE: 유효한 날짜인지 확인
SELECT ISDATE('2025-02-29'); -- 결과: 0 (무효)
SELECT ISDATE('2024-02-29'); -- 결과: 1 (유효, 윤년)

-- SWITCHOFFSET: 시간대 변경
SELECT SWITCHOFFSET(SYSDATETIMEOFFSET(), '+09:00');
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  7. 실무 변환 예제&lt;/h2&gt;
                
                &lt;p&gt;실제 업무에서 자주 사용되는 날짜 처리를 각 데이터베이스별로 구현해보겠습니다.&lt;/p&gt;
                
                &lt;div class=&quot;highlight-box&quot;&gt;
                    &lt;h4&gt;  예제 시나리오: 고객 가입 기간 및 생일 분석&lt;/h4&gt;
                    &lt;p&gt;고객의 나이, 가입 기간, 다음 생일까지의 일수를 계산하는 쿼리를 각 데이터베이스별로 작성해보겠습니다.&lt;/p&gt;
                &lt;/div&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon oracle-icon&quot;&gt;&lt;/span&gt;Oracle 버전&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;Oracle&quot;&gt;
SELECT 
    customer_id,
    customer_name,
    birth_date,
    register_date,
    -- 나이 계산
    FLOOR(MONTHS_BETWEEN(SYSDATE, birth_date) / 12) AS age,
    -- 가입 기간 (일)
    FLOOR(SYSDATE - register_date) AS days_since_registration,
    -- 가입 기간 (월)
    FLOOR(MONTHS_BETWEEN(SYSDATE, register_date)) AS months_since_registration,
    -- 이번 년도 생일
    ADD_MONTHS(birth_date, 
        FLOOR(MONTHS_BETWEEN(SYSDATE, birth_date) / 12) * 12) AS this_year_birthday,
    -- 다음 생일까지 일수
    CASE 
        WHEN ADD_MONTHS(birth_date, 
            FLOOR(MONTHS_BETWEEN(SYSDATE, birth_date) / 12) * 12) &gt;= TRUNC(SYSDATE)
        THEN ADD_MONTHS(birth_date, 
            FLOOR(MONTHS_BETWEEN(SYSDATE, birth_date) / 12) * 12) - TRUNC(SYSDATE)
        ELSE ADD_MONTHS(birth_date, 
            FLOOR(MONTHS_BETWEEN(SYSDATE, birth_date) / 12) * 12 + 12) - TRUNC(SYSDATE)
    END AS days_to_birthday,
    -- 분기별 가입 정보
    'Q' || TO_CHAR(register_date, 'Q') || ' ' || TO_CHAR(register_date, 'YYYY') AS register_quarter
FROM customers;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mysql-icon&quot;&gt;&lt;/span&gt;MySQL 버전&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MySQL&quot;&gt;
SELECT 
    customer_id,
    customer_name,
    birth_date,
    register_date,
    -- 나이 계산
    TIMESTAMPDIFF(YEAR, birth_date, NOW()) AS age,
    -- 가입 기간 (일)
    DATEDIFF(NOW(), register_date) AS days_since_registration,
    -- 가입 기간 (월)
    TIMESTAMPDIFF(MONTH, register_date, NOW()) AS months_since_registration,
    -- 이번 년도 생일
    DATE(CONCAT(YEAR(NOW()), '-', 
        LPAD(MONTH(birth_date), 2, '0'), '-', 
        LPAD(DAY(birth_date), 2, '0'))) AS this_year_birthday,
    -- 다음 생일까지 일수
    CASE 
        WHEN DATE(CONCAT(YEAR(NOW()), '-', 
            LPAD(MONTH(birth_date), 2, '0'), '-', 
            LPAD(DAY(birth_date), 2, '0'))) &gt;= CURDATE()
        THEN DATEDIFF(
            DATE(CONCAT(YEAR(NOW()), '-', 
                LPAD(MONTH(birth_date), 2, '0'), '-', 
                LPAD(DAY(birth_date), 2, '0'))), 
            CURDATE())
        ELSE DATEDIFF(
            DATE(CONCAT(YEAR(NOW()) + 1, '-', 
                LPAD(MONTH(birth_date), 2, '0'), '-', 
                LPAD(DAY(birth_date), 2, '0'))), 
            CURDATE())
    END AS days_to_birthday,
    -- 분기별 가입 정보
    CONCAT('Q', QUARTER(register_date), ' ', YEAR(register_date)) AS register_quarter
FROM customers;
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;&lt;span class=&quot;icon mssql-icon&quot;&gt;&lt;/span&gt;MS SQL Server 버전&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;MSSQL&quot;&gt;
SELECT 
    customer_id,
    customer_name,
    birth_date,
    register_date,
    -- 나이 계산
    DATEDIFF(year, birth_date, GETDATE()) - 
    CASE WHEN DATEADD(year, DATEDIFF(year, birth_date, GETDATE()), birth_date) &gt; GETDATE() 
         THEN 1 ELSE 0 END AS age,
    -- 가입 기간 (일)
    DATEDIFF(day, register_date, GETDATE()) AS days_since_registration,
    -- 가입 기간 (월)
    DATEDIFF(month, register_date, GETDATE()) AS months_since_registration,
    -- 이번 년도 생일
    DATEFROMPARTS(YEAR(GETDATE()), MONTH(birth_date), DAY(birth_date)) AS this_year_birthday,
    -- 다음 생일까지 일수
    CASE 
        WHEN DATEFROMPARTS(YEAR(GETDATE()), MONTH(birth_date), DAY(birth_date)) &gt;= CAST(GETDATE() AS DATE)
        THEN DATEDIFF(day, CAST(GETDATE() AS DATE), 
             DATEFROMPARTS(YEAR(GETDATE()), MONTH(birth_date), DAY(birth_date)))
        ELSE DATEDIFF(day, CAST(GETDATE() AS DATE), 
             DATEFROMPARTS(YEAR(GETDATE()) + 1, MONTH(birth_date), DAY(birth_date)))
    END AS days_to_birthday,
    -- 분기별 가입 정보
    'Q' + CAST(DATEPART(QUARTER, register_date) AS VARCHAR) + ' ' + 
    CAST(YEAR(register_date) AS VARCHAR) AS register_quarter
FROM customers;
                        &lt;/div&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;⚡ 8. 성능 고려사항&lt;/h2&gt;
                
                &lt;div class=&quot;tip-box&quot;&gt;
                    날짜 함수 사용 시 성능을 고려해야 할 중요한 포인트들입니다.
                &lt;/div&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;  인덱스 활용 팁&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;SQL&quot;&gt;
-- ❌ 나쁜 예: 함수를 인덱스된 열에 사용
SELECT * FROM orders 
WHERE YEAR(order_date) = 2025;

-- ✅ 좋은 예: 범위 조건 사용
SELECT * FROM orders 
WHERE order_date &gt;= '2025-01-01' 
  AND order_date &lt; '2026-01-01';
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;  대용량 데이터 처리&lt;/h3&gt;
                        &lt;div class=&quot;code-block&quot; data-lang=&quot;SQL&quot;&gt;
-- 파티셔닝을 고려한 날짜 조건
SELECT * FROM large_table 
WHERE date_column &gt;= DATEADD(month, -1, GETDATE())
  AND date_column &lt; GETDATE();

-- 적절한 날짜 인덱스 생성
CREATE INDEX IX_orders_date 
ON orders(order_date) 
INCLUDE (customer_id, total_amount);
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;  최적화 기법&lt;/h3&gt;
                        &lt;ul&gt;
                            &lt;li&gt;&lt;strong&gt;날짜 리터럴 사용&lt;/strong&gt;: 문자열보다 날짜 리터럴 사용&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;함수 인덱스&lt;/strong&gt;: 자주 사용하는 날짜 함수에 대해 함수 기반 인덱스 생성&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;파티셔닝&lt;/strong&gt;: 대용량 테이블의 날짜 기반 파티셔닝&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;계산된 열&lt;/strong&gt;: 복잡한 날짜 계산은 계산된 열로 저장&lt;/li&gt;
                        &lt;/ul&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  9. 마이그레이션 가이드&lt;/h2&gt;
                
                &lt;p&gt;데이터베이스 간 마이그레이션 시 날짜 함수 변환을 위한 실용적인 가이드입니다.&lt;/p&gt;
                
                &lt;table class=&quot;comparison-table&quot;&gt;
                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;마이그레이션 방향&lt;/th&gt;
                            &lt;th&gt;주요 변경사항&lt;/th&gt;
                            &lt;th&gt;주의사항&lt;/th&gt;
                        &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;Oracle → MySQL&lt;/strong&gt;&lt;/td&gt;
                            &lt;td&gt;
                                • SYSDATE → NOW()&lt;br/&gt;
                                • TO_CHAR → DATE_FORMAT&lt;br/&gt;
                                • ADD_MONTHS → DATE_ADD&lt;br/&gt;
                                • TRUNC → DATE 함수
                            &lt;/td&gt;
                            &lt;td&gt;
                                • Oracle의 날짜 연산(+/-) 불가&lt;br/&gt;
                                • 포맷 문자열 완전히 다름&lt;br/&gt;
                                • DUAL 테이블 불필요
                            &lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;Oracle → MS SQL&lt;/strong&gt;&lt;/td&gt;
                            &lt;td&gt;
                                • SYSDATE → GETDATE()&lt;br/&gt;
                                • TO_CHAR → FORMAT&lt;br/&gt;
                                • ADD_MONTHS → DATEADD&lt;br/&gt;
                                • MONTHS_BETWEEN → DATEDIFF
                            &lt;/td&gt;
                            &lt;td&gt;
                                • 날짜 연산 방식 완전 변경&lt;br/&gt;
                                • FROM dual 제거 필요&lt;br/&gt;
                                • 데이터 타입 매핑 주의
                            &lt;/td&gt;
                        &lt;/tr&gt;
                        &lt;tr&gt;
                            &lt;td&gt;&lt;strong&gt;MySQL → MS SQL&lt;/strong&gt;&lt;/td&gt;
                            &lt;td&gt;
                                • NOW() → GETDATE()&lt;br/&gt;
                                • DATE_FORMAT → FORMAT&lt;br/&gt;
                                • DATE_ADD → DATEADD&lt;br/&gt;
                                • CURDATE() → CAST(GETDATE() AS DATE)
                            &lt;/td&gt;
                            &lt;td&gt;
                                • LIMIT → TOP 변환&lt;br/&gt;
                                • 백틱(`) → 대괄호[] 변환&lt;br/&gt;
                                • 자동 형변환 차이
                            &lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                
                &lt;div class=&quot;highlight-box&quot;&gt;
                    &lt;h4&gt; ️ 마이그레이션 도구 추천&lt;/h4&gt;
                    &lt;ul&gt;
                        &lt;li&gt;&lt;strong&gt;AWS SCT (Schema Conversion Tool)&lt;/strong&gt;: 다양한 DB 간 스키마 변환&lt;/li&gt;
                        &lt;li&gt;&lt;strong&gt;Microsoft SSMA&lt;/strong&gt;: Oracle/MySQL → SQL Server 마이그레이션&lt;/li&gt;
                        &lt;li&gt;&lt;strong&gt;DBConvert&lt;/strong&gt;: 상용 마이그레이션 도구&lt;/li&gt;
                        &lt;li&gt;&lt;strong&gt;정규식 기반 스크립트&lt;/strong&gt;: 단순 함수 변환용&lt;/li&gt;
                    &lt;/ul&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;section&quot;&gt;
                &lt;h2&gt;  10. 실무 베스트 프랙티스&lt;/h2&gt;
                
                &lt;div class=&quot;feature-grid&quot;&gt;
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;✅ 권장사항&lt;/h3&gt;
                        &lt;ul&gt;
                            &lt;li&gt;&lt;strong&gt;표준 SQL 우선 사용&lt;/strong&gt;: CURRENT_TIMESTAMP, EXTRACT 등&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;명시적 형변환&lt;/strong&gt;: 암시적 변환에 의존하지 말 것&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;타임존 고려&lt;/strong&gt;: 글로벌 서비스 시 UTC 기준 사용&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;일관된 형식&lt;/strong&gt;: 프로젝트 내 날짜 형식 통일&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;NULL 처리&lt;/strong&gt;: 날짜 계산 시 NULL 값 고려&lt;/li&gt;
                        &lt;/ul&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;❌ 피해야 할 것들&lt;/h3&gt;
                        &lt;ul&gt;
                            &lt;li&gt;&lt;strong&gt;문자열 날짜 연산&lt;/strong&gt;: '2025-01-01' + 1 같은 연산&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;하드코딩된 형식&lt;/strong&gt;: 지역별 설정에 의존하는 형식&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;복잡한 중첩 함수&lt;/strong&gt;: 가독성과 성능 저하&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;인덱스 컬럼 함수 적용&lt;/strong&gt;: WHERE YEAR(date) = 2025&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;윤년 무시&lt;/strong&gt;: 2월 29일 처리 주의&lt;/li&gt;
                        &lt;/ul&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;feature-card&quot;&gt;
                        &lt;h3&gt;  테스트 체크리스트&lt;/h3&gt;
                        &lt;ul&gt;
                            &lt;li&gt;&lt;strong&gt;윤년 처리&lt;/strong&gt;: 2월 29일 관련 로직&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;월말 처리&lt;/strong&gt;: 31일 → 30일 변환&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;타임존 변환&lt;/strong&gt;: 서머타임 적용&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;NULL 값 처리&lt;/strong&gt;: 날짜가 NULL인 경우&lt;/li&gt;
                            &lt;li&gt;&lt;strong&gt;경계값 테스트&lt;/strong&gt;: 년도 변경, 월 변경 시점&lt;/li&gt;
                        &lt;/ul&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        
        &lt;div class=&quot;footer&quot;&gt;
            &lt;p&gt;  이 가이드가 데이터베이스 간 날짜 함수 변환에 도움이 되길 바랍니다!&lt;/p&gt;
            &lt;p&gt;지속적인 학습과 실무 적용을 통해 더 나은 개발자가 되어보세요.  &lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;script&gt;
        // 간단한 인터랙션 효과
        document.querySelectorAll('.feature-card').forEach(card =&gt; {
            card.addEventListener('mouseenter', function() {
                this.style.transform = 'translateY(-8px) scale(1.02)';
            });
            
            card.addEventListener('mouseleave', function() {
                this.style.transform = 'translateY(0) scale(1)';
            });
        });

        // 코드 블록 복사 기능
        document.querySelectorAll('.code-block').forEach(block =&gt; {
            block.addEventListener('click', function() {
                const text = this.textContent;
                navigator.clipboard.writeText(text).then(() =&gt; {
                    const originalBorder = this.style.borderLeft;
                    this.style.borderLeft = '4px solid #27ae60';
                    setTimeout(() =&gt; {
                        this.style.borderLeft = originalBorder;
                    }, 1000);
                });
            });
        });

        // 부드러운 스크롤 효과
        document.querySelectorAll('a[href^=&quot;#&quot;]').forEach(anchor =&gt; {
            anchor.addEventListener('click', function (e) {
                e.preventDefault();
                document.querySelector(this.getAttribute('href')).scrollIntoView({
                    behavior: 'smooth'
                });
            });
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</description>
      <category>DB</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/132</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-MySQL-MSSQL-%EB%82%A0%EC%A7%9C-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EB%B9%84%EA%B5%90-%EA%B0%80%EC%9D%B4%EB%93%9C#entry132comment</comments>
      <pubDate>Sat, 24 May 2025 22:01:06 +0900</pubDate>
    </item>
    <item>
      <title>MS SQL Server의 고유 함수 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/MS-SQL-Server%EC%9D%98-%EA%B3%A0%EC%9C%A0-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5j2mG/btsOaD4fFxV/5jlNkiUzROxLa6iw66U1fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5j2mG/btsOaD4fFxV/5jlNkiUzROxLa6iw66U1fK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5j2mG/btsOaD4fFxV/5jlNkiUzROxLa6iw66U1fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5j2mG%2FbtsOaD4fFxV%2F5jlNkiUzROxLa6iw66U1fK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;287&quot; height=&quot;177&quot; data-filename=&quot;img.png&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Microsoft SQL Server는 Windows 환경에 최적화된 강력한 기업용 데이터베이스 시스템으로, .NET 생태계와의 완벽한 통합을 통해 독특하고 강력한 함수들을 제공합니다. 이러한 함수들은 비즈니스 애플리케이션 개발에서 복잡한 로직을 간단하게 처리하고, 개발 생산성을 크게 향상시킵니다. 엔터프라이즈 환경에서 자주 사용되는 MS SQL Server만의 독특한 함수들을 실제 사용 예제와 함께 상세히 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. NEWID()와 NEWSEQUENTIALID() - 기업용 고유 식별자 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server의 &lt;b&gt;GUID 생성 함수&lt;/b&gt;들은 분산 시스템과 엔터프라이즈 환경에서 데이터 무결성을 보장하는 핵심 도구입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NEWID() - 무작위 GUID 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NEWID()는 RFC 4122 표준을 준수하는 완전히 무작위적인 GUID를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT NEWID() AS random_guid;
-- 결과 예: 6F9619FF-8B86-D011-B42D-00C04FC964FF

-- 데이터 타입: uniqueidentifier (16바이트)
DECLARE @guid uniqueidentifier = NEWID();
SELECT @guid AS my_guid;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NEWSEQUENTIALID() - 순차적 GUID 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NEWSEQUENTIALID()는 인덱스 성능을 고려한 순차적 GUID를 생성하여 페이지 분할을 최소화합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 테이블 생성 시에만 사용 가능 (DEFAULT 제약 조건)
CREATE TABLE Orders (
    OrderID uniqueidentifier DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
    CustomerID int,
    OrderDate datetime2 DEFAULT GETDATE(),
    TotalAmount decimal(10,2)
);

-- 데이터 삽입 (OrderID는 자동 생성)
INSERT INTO Orders (CustomerID, TotalAmount)
VALUES (1001, 50000.00), (1002, 75000.00);

-- 결과 확인
SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM Orders
ORDER BY OrderDate;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 문서 관리 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 문서 버전 관리 테이블
CREATE TABLE DocumentVersions (
    VersionID uniqueidentifier DEFAULT NEWID() PRIMARY KEY,
    DocumentID int,
    VersionNumber int,
    Content nvarchar(max),
    CreatedBy int,
    CreatedDate datetime2 DEFAULT GETDATE(),
    FileSize bigint,
    CheckSum varchar(64)
);

-- 문서 버전 생성 프로시저
CREATE PROCEDURE CreateDocumentVersion
    @DocumentID int,
    @Content nvarchar(max),
    @CreatedBy int,
    @FileSize bigint
AS
BEGIN
    DECLARE @VersionID uniqueidentifier = NEWID();
    DECLARE @VersionNumber int;
    
    -- 다음 버전 번호 계산
    SELECT @VersionNumber = ISNULL(MAX(VersionNumber), 0) + 1
    FROM DocumentVersions
    WHERE DocumentID = @DocumentID;
    
    -- 새 버전 생성
    INSERT INTO DocumentVersions (
        VersionID, DocumentID, VersionNumber, 
        Content, CreatedBy, FileSize, CheckSum
    ) VALUES (
        @VersionID, @DocumentID, @VersionNumber,
        @Content, @CreatedBy, @FileSize, 
        CONVERT(varchar(64), HASHBYTES('SHA2_256', @Content), 2)
    );
    
    -- 생성된 버전 ID 반환
    SELECT @VersionID AS NewVersionID, @VersionNumber AS VersionNumber;
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. IIF() - 간결한 조건부 로직&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;IIF()&lt;/b&gt; 함수는 SQL Server 2012부터 도입된 함수로, 복잡한 CASE 문을 간단한 삼항 연산자 형태로 표현할 수 있게 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 문법&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;IIF(boolean_expression, true_value, false_value)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 직원 성과 평가 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 직원 성과 데이터 조회
SELECT 
    EmployeeID,
    EmployeeName,
    SalesAmount,
    SalesTarget,
    -- 목표 달성 여부
    IIF(SalesAmount &amp;gt;= SalesTarget, '목표 달성', '목표 미달성') AS Achievement,
    -- 성과 등급
    IIF(SalesAmount &amp;gt;= SalesTarget * 1.2, 'A', 
        IIF(SalesAmount &amp;gt;= SalesTarget, 'B', 'C')) AS Grade,
    -- 보너스 계산
    IIF(SalesAmount &amp;gt;= SalesTarget, 
        SalesAmount * 0.1, 
        0) AS BonusAmount,
    -- 상태 표시
    IIF(IsActive = 1, '재직', '퇴사') AS EmploymentStatus
FROM Employees
WHERE DepartmentID = 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 재고 관리 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 상품 재고 상태 분석
SELECT 
    ProductID,
    ProductName,
    CurrentStock,
    MinimumStock,
    MaximumStock,
    -- 재고 상태
    IIF(CurrentStock &amp;lt;= MinimumStock, '부족', 
        IIF(CurrentStock &amp;gt;= MaximumStock, '과다', '적정')) AS StockStatus,
    -- 주문 필요 여부
    IIF(CurrentStock &amp;lt;= MinimumStock, '주문 필요', '주문 불필요') AS OrderNeeded,
    -- 권장 주문 수량
    IIF(CurrentStock &amp;lt;= MinimumStock, 
        MaximumStock - CurrentStock, 
        0) AS RecommendedOrderQty,
    -- 재고 회전율 등급
    IIF(TurnoverRate &amp;gt;= 12, '높음',
        IIF(TurnoverRate &amp;gt;= 6, '보통', '낮음')) AS TurnoverGrade
FROM Products
WHERE CategoryID IN (1, 2, 3);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. CHOOSE() - 인덱스 기반 값 선택&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CHOOSE()&lt;/b&gt; 함수는 인덱스 번호에 따라 값 목록에서 해당 위치의 값을 선택하는 독특한 함수입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 문법&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;CHOOSE(index, value1, value2, value3, ...)
-- index는 1부터 시작
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 다국어 지원 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 다국어 메시지 처리
CREATE FUNCTION GetLocalizedMessage(
    @MessageCode int,
    @LanguageCode int  -- 1: 한국어, 2: 영어, 3: 일본어, 4: 중국어
)
RETURNS nvarchar(200)
AS
BEGIN
    DECLARE @Message nvarchar(200);
    
    SET @Message = CASE @MessageCode
        WHEN 1001 THEN CHOOSE(@LanguageCode, '로그인 성공', 'Login Success', 'ログイン成功', '登录成功')
        WHEN 1002 THEN CHOOSE(@LanguageCode, '로그인 실패', 'Login Failed', 'ログイン失敗', '登录失败')
        WHEN 1003 THEN CHOOSE(@LanguageCode, '권한이 없습니다', 'Access Denied', 'アクセス拒否', '权限不足')
        WHEN 1004 THEN CHOOSE(@LanguageCode, '데이터가 저장되었습니다', 'Data Saved', 'データが保存されました', '数据已保存')
        ELSE CHOOSE(@LanguageCode, '알 수 없는 오류', 'Unknown Error', '不明なエラー', '未知错误')
    END;
    
    RETURN @Message;
END;

-- 사용 예제
SELECT 
    dbo.GetLocalizedMessage(1001, 1) AS Korean,
    dbo.GetLocalizedMessage(1001, 2) AS English,
    dbo.GetLocalizedMessage(1001, 3) AS Japanese,
    dbo.GetLocalizedMessage(1001, 4) AS Chinese;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 비즈니스 규칙 엔진&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;-- 고객 등급별 할인율 적용
SELECT 
    CustomerID,
    CustomerName,
    CustomerGrade,  -- 1: 브론즈, 2: 실버, 3: 골드, 4: 플래티넘, 5: 다이아몬드
    OrderAmount,
    -- 등급별 할인율
    CHOOSE(CustomerGrade, 0.02, 0.05, 0.08, 0.12, 0.15) AS DiscountRate,
    -- 등급별 배송비
    CHOOSE(CustomerGrade, 3000, 2000, 1000, 0, 0) AS ShippingFee,
    -- 등급별 적립률
    CHOOSE(CustomerGrade, 0.01, 0.015, 0.02, 0.025, 0.03) AS PointRate,
    -- 할인 적용 후 금액
    OrderAmount * (1 - CHOOSE(CustomerGrade, 0.02, 0.05, 0.08, 0.12, 0.15)) AS DiscountedAmount
FROM Orders o
JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE o.OrderDate &amp;gt;= DATEADD(month, -1, GETDATE());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. FORMAT() - .NET 스타일 데이터 포맷팅&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;FORMAT()&lt;/b&gt; 함수는 .NET Framework의 강력한 포맷팅 기능을 SQL Server에서 사용할 수 있게 해주는 독특한 함수입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다양한 포맷팅 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 비즈니스 리포트 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 월별 매출 리포트
SELECT 
    FORMAT(OrderDate, 'yyyy-MM') AS SalesMonth,
    FORMAT(OrderDate, 'MMMM yyyy', 'ko-KR') AS SalesMonthKorean,
    COUNT(*) AS OrderCount,
    -- 통화 포맷팅
    FORMAT(SUM(TotalAmount), 'C', 'ko-KR') AS TotalSalesKRW,
    FORMAT(SUM(TotalAmount), 'C', 'en-US') AS TotalSalesUSD,
    -- 숫자 포맷팅
    FORMAT(AVG(TotalAmount), 'N2', 'ko-KR') AS AverageOrderAmount,
    -- 퍼센트 포맷팅
    FORMAT(
        SUM(TotalAmount) * 1.0 / 
        (SELECT SUM(TotalAmount) FROM Orders WHERE YEAR(OrderDate) = YEAR(GETDATE())), 
        'P2', 'ko-KR'
    ) AS MonthlyContribution
FROM Orders
WHERE YEAR(OrderDate) = YEAR(GETDATE())
GROUP BY FORMAT(OrderDate, 'yyyy-MM'), FORMAT(OrderDate, 'MMMM yyyy', 'ko-KR')
ORDER BY SalesMonth;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 고객 정보 포맷팅&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 고객 연락처 및 정보 포맷팅
SELECT 
    CustomerID,
    CustomerName,
    -- 전화번호 포맷팅 (한국 형식)
    FORMAT(CAST(Phone AS bigint), '000-0000-0000') AS FormattedPhone,
    -- 생년월일 포맷팅
    FORMAT(BirthDate, 'yyyy년 MM월 dd일', 'ko-KR') AS BirthDateKorean,
    FORMAT(BirthDate, 'MMMM dd, yyyy', 'en-US') AS BirthDateEnglish,
    -- 나이 계산 및 포맷팅
    FORMAT(DATEDIFF(year, BirthDate, GETDATE()), 'N0') + '세' AS Age,
    -- 등록일 포맷팅
    FORMAT(RegisterDate, 'yyyy-MM-dd HH:mm:ss') AS RegisterDateTime,
    -- 고객 번호 포맷팅 (앞자리 0 추가)
    FORMAT(CustomerID, '000000') AS FormattedCustomerID
FROM Customers
WHERE IsActive = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. STRING_SPLIT()과 STRING_AGG() - 문자열 처리의 강력한 도구&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL Server 2016부터 도입된 이 함수들은 문자열 데이터를 효율적으로 처리하는 현대적인 방법을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STRING_SPLIT() 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 태그 기반 상품 검색&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 태그 기반 상품 검색 시스템
CREATE PROCEDURE SearchProductsByTags
    @SearchTags nvarchar(500)  -- 예: 'electronics,smartphone,samsung'
AS
BEGIN
    -- 검색할 태그들을 테이블로 변환
    WITH SearchTagsTable AS (
        SELECT TRIM(value) AS TagName
        FROM STRING_SPLIT(@SearchTags, ',')
        WHERE TRIM(value) &amp;lt;&amp;gt; ''
    ),
    -- 태그별 상품 매칭
    ProductMatches AS (
        SELECT 
            p.ProductID,
            p.ProductName,
            p.Price,
            p.CategoryID,
            COUNT(st.TagName) AS MatchingTagCount
        FROM Products p
        JOIN ProductTags pt ON p.ProductID = pt.ProductID
        JOIN Tags t ON pt.TagID = t.TagID
        JOIN SearchTagsTable st ON t.TagName = st.TagName
        GROUP BY p.ProductID, p.ProductName, p.Price, p.CategoryID
    )
    -- 매칭된 태그 수에 따라 정렬하여 결과 반환
    SELECT 
        pm.ProductID,
        pm.ProductName,
        FORMAT(pm.Price, 'C', 'ko-KR') AS FormattedPrice,
        pm.MatchingTagCount,
        -- 해당 상품의 모든 태그
        STRING_AGG(t.TagName, ', ') AS AllTags
    FROM ProductMatches pm
    JOIN ProductTags pt ON pm.ProductID = pt.ProductID
    JOIN Tags t ON pt.TagID = t.TagID
    GROUP BY pm.ProductID, pm.ProductName, pm.Price, pm.MatchingTagCount
    ORDER BY pm.MatchingTagCount DESC, pm.ProductName;
END;

-- 사용 예제
EXEC SearchProductsByTags 'electronics,smartphone,samsung';
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;STRING_AGG() 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 조직 구조 리포트&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 부서별 직원 현황 리포트
SELECT 
    d.DepartmentName,
    d.DepartmentCode,
    COUNT(e.EmployeeID) AS EmployeeCount,
    -- 직원 목록 (이름만)
    STRING_AGG(e.EmployeeName, ', ') AS EmployeeList,
    -- 직원 목록 (직급 포함)
    STRING_AGG(
        CONCAT(e.EmployeeName, '(', p.PositionName, ')'), 
        ' | '
    ) AS DetailedEmployeeList,
    -- 급여 통계
    FORMAT(AVG(e.Salary), 'C', 'ko-KR') AS AverageSalary,
    FORMAT(SUM(e.Salary), 'C', 'ko-KR') AS TotalSalary,
    -- 직급별 인원수
    STRING_AGG(
        CONCAT(p.PositionName, ': ', COUNT(e.EmployeeID)), 
        ', '
    ) AS PositionSummary
FROM Departments d
LEFT JOIN Employees e ON d.DepartmentID = e.DepartmentID
LEFT JOIN Positions p ON e.PositionID = p.PositionID
WHERE e.IsActive = 1
GROUP BY d.DepartmentID, d.DepartmentName, d.DepartmentCode
ORDER BY d.DepartmentName;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. DATEDIFF()와 EOMONTH() - 날짜 처리의 정밀함&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server의 &lt;b&gt;날짜 함수&lt;/b&gt;들은 정밀한 날짜 계산과 비즈니스 로직 구현에 필수적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 고객 생애 가치 분석&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 고객 행동 분석 및 생애 가치 계산
SELECT 
    c.CustomerID,
    c.CustomerName,
    c.RegisterDate,
    -- 가입 기간 계산
    DATEDIFF(day, c.RegisterDate, GETDATE()) AS DaysSinceRegistration,
    DATEDIFF(month, c.RegisterDate, GETDATE()) AS MonthsSinceRegistration,
    DATEDIFF(year, c.RegisterDate, GETDATE()) AS YearsSinceRegistration,
    
    -- 주문 관련 정보
    COUNT(o.OrderID) AS TotalOrders,
    ISNULL(SUM(o.TotalAmount), 0) AS TotalSpent,
    
    -- 최근 주문 정보
    MAX(o.OrderDate) AS LastOrderDate,
    DATEDIFF(day, MAX(o.OrderDate), GETDATE()) AS DaysSinceLastOrder,
    
    -- 고객 활동성 분석
    IIF(DATEDIFF(day, MAX(o.OrderDate), GETDATE()) &amp;lt;= 30, '활성', 
        IIF(DATEDIFF(day, MAX(o.OrderDate), GETDATE()) &amp;lt;= 90, '비활성', '휴면')) AS CustomerStatus,
    
    -- 월평균 구매 금액
    CASE 
        WHEN DATEDIFF(month, c.RegisterDate, GETDATE()) &amp;gt; 0 
        THEN ISNULL(SUM(o.TotalAmount), 0) / DATEDIFF(month, c.RegisterDate, GETDATE())
        ELSE ISNULL(SUM(o.TotalAmount), 0)
    END AS MonthlyAverageSpend,
    
    -- 생애 가치 등급
    CHOOSE(
        CASE 
            WHEN ISNULL(SUM(o.TotalAmount), 0) &amp;gt;= 1000000 THEN 5
            WHEN ISNULL(SUM(o.TotalAmount), 0) &amp;gt;= 500000 THEN 4
            WHEN ISNULL(SUM(o.TotalAmount), 0) &amp;gt;= 200000 THEN 3
            WHEN ISNULL(SUM(o.TotalAmount), 0) &amp;gt;= 50000 THEN 2
            ELSE 1
        END,
        'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond'
    ) AS CustomerTier
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID, c.CustomerName, c.RegisterDate
ORDER BY TotalSpent DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 월별 매출 분석 및 예측&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 월별 매출 추이 및 다음 달 예측
WITH MonthlySales AS (
    SELECT 
        YEAR(OrderDate) AS SalesYear,
        MONTH(OrderDate) AS SalesMonth,
        EOMONTH(OrderDate) AS MonthEnd,
        COUNT(*) AS OrderCount,
        SUM(TotalAmount) AS MonthlySales,
        AVG(TotalAmount) AS AverageOrderValue
    FROM Orders
    WHERE OrderDate &amp;gt;= DATEADD(year, -2, GETDATE())
    GROUP BY YEAR(OrderDate), MONTH(OrderDate), EOMONTH(OrderDate)
),
SalesWithGrowth AS (
    SELECT 
        *,
        LAG(MonthlySales) OVER (ORDER BY SalesYear, SalesMonth) AS PreviousMonthSales,
        LAG(MonthlySales, 12) OVER (ORDER BY SalesYear, SalesMonth) AS SameMonthLastYear
    FROM MonthlySales
)
SELECT 
    SalesYear,
    SalesMonth,
    FORMAT(MonthEnd, 'yyyy년 MM월', 'ko-KR') AS MonthDisplay,
    OrderCount,
    FORMAT(MonthlySales, 'C', 'ko-KR') AS FormattedMonthlySales,
    FORMAT(AverageOrderValue, 'C', 'ko-KR') AS FormattedAOV,
    
    -- 전월 대비 증감
    IIF(PreviousMonthSales IS NOT NULL,
        FORMAT((MonthlySales - PreviousMonthSales) / PreviousMonthSales, 'P2', 'ko-KR'),
        'N/A') AS MonthOverMonthGrowth,
    
    -- 전년 동월 대비 증감
    IIF(SameMonthLastYear IS NOT NULL,
        FORMAT((MonthlySales - SameMonthLastYear) / SameMonthLastYear, 'P2', 'ko-KR'),
        'N/A') AS YearOverYearGrowth,
    
    -- 월별 성과 등급
    CHOOSE(
        CASE 
            WHEN MonthlySales &amp;gt;= 10000000 THEN 5
            WHEN MonthlySales &amp;gt;= 7000000 THEN 4
            WHEN MonthlySales &amp;gt;= 5000000 THEN 3
            WHEN MonthlySales &amp;gt;= 3000000 THEN 2
            ELSE 1
        END,
        '매우 저조', '저조', '보통', '우수', '매우 우수'
    ) AS PerformanceGrade
FROM SalesWithGrowth
ORDER BY SalesYear DESC, SalesMonth DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. HASHBYTES()와 보안 함수들 - 엔터프라이즈 보안&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server는 &lt;b&gt;데이터 보안과 무결성&lt;/b&gt;을 위한 강력한 암호화 및 해시 함수들을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제: 사용자 인증 및 데이터 무결성 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 사용자 인증 테이블
CREATE TABLE UserAccounts (
    UserID int IDENTITY(1,1) PRIMARY KEY,
    Username nvarchar(50) UNIQUE NOT NULL,
    Email nvarchar(100) UNIQUE NOT NULL,
    PasswordHash varbinary(64) NOT NULL,
    PasswordSalt uniqueidentifier DEFAULT NEWID() NOT NULL,
    CreatedDate datetime2 DEFAULT GETDATE(),
    LastLoginDate datetime2,
    FailedLoginAttempts int DEFAULT 0,
    IsLocked bit DEFAULT 0
);

-- 안전한 패스워드 해시 생성 함수
CREATE FUNCTION GeneratePasswordHash(
    @Password nvarchar(100),
    @Salt uniqueidentifier
)
RETURNS varbinary(64)
AS
BEGIN
    RETURN HASHBYTES('SHA2_512', @Password + CAST(@Salt AS nvarchar(36)));
END;

-- 사용자 등록 프로시저
CREATE PROCEDURE RegisterUser
    @Username nvarchar(50),
    @Email nvarchar(100),
    @Password nvarchar(100)
AS
BEGIN
    DECLARE @Salt uniqueidentifier = NEWID();
    DECLARE @PasswordHash varbinary(64) = dbo.GeneratePasswordHash(@Password, @Salt);
    
    INSERT INTO UserAccounts (Username, Email, PasswordHash, PasswordSalt)
    VALUES (@Username, @Email, @PasswordHash, @Salt);
    
    SELECT SCOPE_IDENTITY() AS NewUserID;
END;

-- 사용자 로그인 검증 프로시저
CREATE PROCEDURE AuthenticateUser
    @Username nvarchar(50),
    @Password nvarchar(100)
AS
BEGIN
    DECLARE @UserID int;
    DECLARE @StoredHash varbinary(64);
    DECLARE @Salt uniqueidentifier;
    DECLARE @FailedAttempts int;
    DECLARE @IsLocked bit;
    
    -- 사용자 정보 조회
    SELECT 
        @UserID = UserID,
        @StoredHash = PasswordHash,
        @Salt = PasswordSalt,
        @FailedAttempts = FailedLoginAttempts,
        @IsLocked = IsLocked
    FROM UserAccounts
    WHERE Username = @Username;
    
    -- 사용자 존재 여부 확인
    IF @UserID IS NULL
    BEGIN
        SELECT 'USER_NOT_FOUND' AS Result;
        RETURN;
    END
    
    -- 계정 잠금 확인
    IF @IsLocked = 1
    BEGIN
        SELECT 'ACCOUNT_LOCKED' AS Result;
        RETURN;
    END
    
    -- 패스워드 검증
    IF dbo.GeneratePasswordHash(@Password, @Salt) = @StoredHash
    BEGIN
        -- 로그인 성공
        UPDATE UserAccounts 
        SET LastLoginDate = GETDATE(), 
            FailedLoginAttempts = 0
        WHERE UserID = @UserID;
        
        SELECT 'SUCCESS' AS Result, @UserID AS UserID;
    END
    ELSE
    BEGIN
        -- 로그인 실패
        SET @FailedAttempts = @FailedAttempts + 1;
        
        UPDATE UserAccounts 
        SET FailedLoginAttempts = @FailedAttempts,
            IsLocked = IIF(@FailedAttempts &amp;gt;= 5, 1, 0)
        WHERE UserID = @UserID;
        
        SELECT IIF(@FailedAttempts &amp;gt;= 5, 'ACCOUNT_LOCKED', 'INVALID_PASSWORD') AS Result;
    END
END;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MS SQL Server 고유 함수의 기업 가치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server의 고유 함수들은 엔터프라이즈 환경에서 요구되는 복잡한 비즈니스 로직, 보안 요구사항, 그리고 성능 최적화를 효과적으로 해결합니다. NEWID()와 NEWSEQUENTIALID()는 분산 시스템에서의 데이터 무결성을 보장하고, IIF()와 CHOOSE()는 복잡한 비즈니스 규칙을 간결하게 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 FORMAT() 함수를 통한 .NET 스타일의 데이터 포맷팅, STRING_SPLIT()과 STRING_AGG()를 활용한 현대적 문자열 처리, 그리고 HASHBYTES()를 통한 보안 강화는 현대 비즈니스 애플리케이션의 요구사항을 완벽히 충족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 함수들은 단순히 코딩의 편의성을 제공하는 것을 넘어서, 기업의 핵심 비즈니스 로직을 데이터베이스 레벨에서 효율적으로 구현할 수 있게 해줍니다. MS SQL Server의 지속적인 발전과 함께 새로운 함수들이 계속 추가되고 있으므로, 최신 버전의 기능들을 적극적으로 활용하여 더욱 강력하고 안전한 엔터프라이즈 애플리케이션을 구축할 수 있습니다.&lt;/p&gt;</description>
      <category>DB/MSSQL</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/131</guid>
      <comments>https://devsite.tistory.com/entry/MS-SQL-Server%EC%9D%98-%EA%B3%A0%EC%9C%A0-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry131comment</comments>
      <pubDate>Sat, 24 May 2025 21:59:41 +0900</pubDate>
    </item>
    <item>
      <title>MySQL 데이터베이스의 고유 함수 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/MySQL-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%9D%98-%EA%B3%A0%EC%9C%A0-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1737&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Aj6ps/btsOaE29XeI/iCGb3kNuNNFOKNLfmWnTQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Aj6ps/btsOaE29XeI/iCGb3kNuNNFOKNLfmWnTQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Aj6ps/btsOaE29XeI/iCGb3kNuNNFOKNLfmWnTQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAj6ps%2FbtsOaE29XeI%2FiCGb3kNuNNFOKNLfmWnTQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;362&quot; height=&quot;241&quot; data-filename=&quot;1.png&quot; data-origin-width=&quot;1737&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 세계에서 가장 널리 사용되는 오픈 소스 관계형 데이터베이스 시스템으로, 웹 애플리케이션 개발에 특화된 다양한 고유 함수들을 제공합니다. 이러한 함수들은 웹 개발자들이 자주 마주치는 실무 상황에서 강력한 도구가 되며, 코드의 간결성과 성능 향상에 크게 기여합니다. MySQL만의 독특하고 유용한 함수들을 실제 사용 예제와 함께 깊이 있게 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. UUID() - 웹 애플리케이션의 필수 고유 식별자&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UUID()&lt;/b&gt; 함수는 MySQL에서 RFC 4122 표준을 준수하는 범용 고유 식별자를 생성합니다. 웹 애플리케이션에서 세션 ID, 파일명, API 키 등을 생성할 때 매우 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 특징과 형식&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT UUID();
-- 결과 예: 550e8400-e29b-41d4-a716-446655440000

-- 하이픈 제거 버전
SELECT REPLACE(UUID(), '-', '');
-- 결과 예: 550e8400e29b41d4a716446655440000
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 UUID()는 하이픈(-)이 포함된 소문자 형식으로 출력되며, 36자의 문자열로 구성됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 사용자 세션 관리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 사용자 로그인 시 세션 생성
INSERT INTO user_sessions (
    session_id,
    user_id,
    created_at,
    expires_at
) VALUES (
    UUID(),
    123,
    NOW(),
    DATE_ADD(NOW(), INTERVAL 24 HOUR)
);

-- 활성 세션 조회
SELECT session_id, user_id, created_at
FROM user_sessions
WHERE expires_at &amp;gt; NOW()
  AND user_id = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 파일 업로드 시 고유 파일명 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 파일 업로드 테이블
CREATE TABLE file_uploads (
    id INT AUTO_INCREMENT PRIMARY KEY,
    original_filename VARCHAR(255),
    stored_filename VARCHAR(100) DEFAULT (CONCAT(REPLACE(UUID(), '-', ''), '.jpg')),
    upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    file_size INT,
    user_id INT
);

-- 파일 업로드 처리
INSERT INTO file_uploads (original_filename, file_size, user_id)
VALUES ('profile_photo.jpg', 2048576, 456);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. UUID_TO_BIN()과 BIN_TO_UUID() - 효율적인 UUID 저장&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0에서 도입된 이 함수들은 UUID를 16바이트 바이너리 형식으로 변환하여 저장 공간을 크게 절약하고 성능을 향상시킵니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;저장 공간 비교&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문자열 UUID&lt;/b&gt;: 36바이트 (하이픈 포함)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;바이너리 UUID&lt;/b&gt;: 16바이트 (약 55% 공간 절약)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 효율적인 주문 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 주문 테이블 생성 (바이너리 UUID 사용)
CREATE TABLE orders (
    order_id BINARY(16) DEFAULT (UUID_TO_BIN(UUID())),
    customer_id INT,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10,2),
    INDEX idx_order_id (order_id)
);

-- 주문 생성
INSERT INTO orders (customer_id, total_amount)
VALUES (789, 149900.00);

-- 주문 조회 (사람이 읽기 쉬운 형태로)
SELECT BIN_TO_UUID(order_id) AS order_uuid,
       customer_id,
       order_date,
       total_amount
FROM orders
WHERE customer_id = 789
ORDER BY order_date DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: API 키 관리 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- API 키 테이블
CREATE TABLE api_keys (
    id INT AUTO_INCREMENT PRIMARY KEY,
    key_uuid BINARY(16) DEFAULT (UUID_TO_BIN(UUID())),
    user_id INT,
    key_name VARCHAR(100),
    permissions JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP,
    is_active BOOLEAN DEFAULT TRUE
);

-- API 키 생성
INSERT INTO api_keys (user_id, key_name, permissions)
VALUES (123, 'Mobile App Key', '[&quot;read&quot;, &quot;write&quot;]');

-- API 키 검증 (문자열로 받은 키를 바이너리로 변환하여 조회)
SELECT user_id, permissions, is_active
FROM api_keys
WHERE key_uuid = UUID_TO_BIN('550e8400-e29b-41d4-a716-446655440000')
  AND is_active = TRUE
  AND (expires_at IS NULL OR expires_at &amp;gt; NOW());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. GROUP_CONCAT() - 데이터 집계의 강력한 도구&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GROUP_CONCAT()&lt;/b&gt;은 MySQL의 대표적인 고유 함수로, 그룹화된 데이터를 하나의 문자열로 연결합니다. 웹 애플리케이션에서 태그, 카테고리, 권한 등을 표시할 때 매우 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 문법과 옵션&lt;/h3&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;GROUP_CONCAT([DISTINCT] 표현식 
             [ORDER BY 정렬기준] 
             [SEPARATOR '구분자'])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 사용자별 권한 목록&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 사용자 권한 조회
SELECT u.username,
       u.email,
       GROUP_CONCAT(
           p.permission_name 
           ORDER BY p.permission_name 
           SEPARATOR ', '
       ) AS permissions
FROM users u
JOIN user_permissions up ON u.id = up.user_id
JOIN permissions p ON up.permission_id = p.id
WHERE u.is_active = TRUE
GROUP BY u.id, u.username, u.email
ORDER BY u.username;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 상품별 태그 관리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 상품과 태그 정보
SELECT p.product_name,
       p.price,
       GROUP_CONCAT(
           DISTINCT t.tag_name 
           ORDER BY t.tag_name 
           SEPARATOR ' | '
       ) AS tags,
       COUNT(DISTINCT pt.tag_id) AS tag_count
FROM products p
LEFT JOIN product_tags pt ON p.id = pt.product_id
LEFT JOIN tags t ON pt.tag_id = t.id
WHERE p.is_active = TRUE
GROUP BY p.id, p.product_name, p.price
HAVING tag_count &amp;gt; 0
ORDER BY p.product_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 3: 월별 판매 실적 요약&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 각 월의 베스트셀러 상품들을 문자열로 연결
SELECT DATE_FORMAT(order_date, '%Y-%m') AS month,
       COUNT(*) AS total_orders,
       SUM(total_amount) AS total_revenue,
       GROUP_CONCAT(
           DISTINCT CONCAT(p.product_name, ' (', oi.quantity, '개)')
           ORDER BY SUM(oi.quantity * oi.unit_price) DESC
           SEPARATOR ', '
       ) AS top_products
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.order_date &amp;gt;= DATE_SUB(NOW(), INTERVAL 6 MONTH)
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. GET_LOCK()과 RELEASE_LOCK() - 웹 애플리케이션의 동시성 제어&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 &lt;b&gt;사용자 정의 락 함수&lt;/b&gt;들은 웹 애플리케이션에서 동시성 제어와 중복 처리 방지에 매우 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 락 획득 시도 (10초 대기)
SELECT GET_LOCK('lock_name', 10);
-- 반환값: 1(성공), 0(타임아웃), NULL(오류)

-- 락 해제
SELECT RELEASE_LOCK('lock_name');
-- 반환값: 1(성공), 0(해제할 락 없음), NULL(오류)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 이메일 발송 큐 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 이메일 발송 처리 (중복 처리 방지)
DELIMITER $$

CREATE PROCEDURE ProcessEmailQueue()
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE email_id INT;
    DECLARE lock_acquired INT DEFAULT 0;
    
    -- 이메일 큐 처리 락 획득
    SET lock_acquired = GET_LOCK('email_queue_processor', 1);
    
    IF lock_acquired = 1 THEN
        -- 대기 중인 이메일 처리
        SELECT id INTO email_id 
        FROM email_queue 
        WHERE status = 'PENDING' 
        ORDER BY created_at ASC 
        LIMIT 1;
        
        IF email_id IS NOT NULL THEN
            -- 이메일 상태 업데이트
            UPDATE email_queue 
            SET status = 'PROCESSING', 
                processed_at = NOW() 
            WHERE id = email_id;
            
            -- 실제 이메일 발송 로직 (생략)
            -- CALL SendActualEmail(email_id);
            
            -- 발송 완료 처리
            UPDATE email_queue 
            SET status = 'SENT', 
                sent_at = NOW() 
            WHERE id = email_id;
        END IF;
        
        -- 락 해제
        DO RELEASE_LOCK('email_queue_processor');
    END IF;
END$$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 재고 관리 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 상품 주문 시 재고 차감 (동시성 제어)
DELIMITER $$

CREATE PROCEDURE OrderProduct(IN p_product_id INT, IN p_quantity INT, OUT p_result VARCHAR(50))
BEGIN
    DECLARE current_stock INT DEFAULT 0;
    DECLARE lock_name VARCHAR(100);
    DECLARE lock_acquired INT DEFAULT 0;
    
    SET lock_name = CONCAT('product_stock_', p_product_id);
    SET lock_acquired = GET_LOCK(lock_name, 5);
    
    IF lock_acquired = 1 THEN
        -- 현재 재고 확인
        SELECT stock_quantity INTO current_stock
        FROM products 
        WHERE id = p_product_id;
        
        IF current_stock &amp;gt;= p_quantity THEN
            -- 재고 차감
            UPDATE products 
            SET stock_quantity = stock_quantity - p_quantity,
                updated_at = NOW()
            WHERE id = p_product_id;
            
            SET p_result = 'SUCCESS';
        ELSE
            SET p_result = 'INSUFFICIENT_STOCK';
        END IF;
        
        -- 락 해제
        DO RELEASE_LOCK(lock_name);
    ELSE
        SET p_result = 'LOCK_TIMEOUT';
    END IF;
END$$

DELIMITER ;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. INET_ATON()과 INET_NTOA() - IP 주소 처리의 효율성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션에서 IP 주소 로깅과 분석은 필수적입니다. MySQL의 &lt;b&gt;IP 주소 변환 함수&lt;/b&gt;들을 사용하면 효율적으로 IP 데이터를 저장하고 검색할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 개념&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;INET_ATON()&lt;/b&gt;: IP 주소 문자열을 4바이트 정수로 변환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;INET_NTOA()&lt;/b&gt;: 정수를 IP 주소 문자열로 변환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 사용자 접속 로그 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 접속 로그 테이블
CREATE TABLE access_logs (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    ip_address INT UNSIGNED,  -- IP를 정수로 저장
    user_agent TEXT,
    access_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    page_url VARCHAR(500),
    INDEX idx_ip_time (ip_address, access_time),
    INDEX idx_user_time (user_id, access_time)
);

-- 접속 로그 기록
INSERT INTO access_logs (user_id, ip_address, user_agent, page_url)
VALUES (
    123,
    INET_ATON('192.168.1.100'),
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    '/dashboard'
);

-- IP 주소별 접속 통계
SELECT INET_NTOA(ip_address) AS ip_address,
       COUNT(*) AS access_count,
       COUNT(DISTINCT user_id) AS unique_users,
       MIN(access_time) AS first_access,
       MAX(access_time) AS last_access
FROM access_logs
WHERE access_time &amp;gt;= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY ip_address
HAVING access_count &amp;gt; 100  -- 의심스러운 활동 탐지
ORDER BY access_count DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: IP 기반 지역 제한 시스템&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- IP 차단 목록 관리
CREATE TABLE ip_restrictions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ip_start INT UNSIGNED,
    ip_end INT UNSIGNED,
    restriction_type ENUM('BLOCK', 'ALLOW', 'MONITOR'),
    region VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_ip_range (ip_start, ip_end)
);

-- 특정 IP 대역 차단 설정
INSERT INTO ip_restrictions (ip_start, ip_end, restriction_type, region)
VALUES (
    INET_ATON('10.0.0.0'),      -- 시작 IP
    INET_ATON('10.255.255.255'), -- 끝 IP
    'BLOCK',
    'Internal Network'
);

-- IP 접근 권한 확인 함수
DELIMITER $$

CREATE FUNCTION CheckIPAccess(ip_addr VARCHAR(15))
RETURNS VARCHAR(20)
READS SQL DATA
DETERMINISTIC
BEGIN
    DECLARE restriction VARCHAR(20) DEFAULT 'ALLOW';
    DECLARE ip_num INT UNSIGNED;
    
    SET ip_num = INET_ATON(ip_addr);
    
    SELECT restriction_type INTO restriction
    FROM ip_restrictions
    WHERE ip_num BETWEEN ip_start AND ip_end
    ORDER BY 
        CASE restriction_type 
            WHEN 'BLOCK' THEN 1 
            WHEN 'MONITOR' THEN 2 
            WHEN 'ALLOW' THEN 3 
        END
    LIMIT 1;
    
    RETURN restriction;
END$$

DELIMITER ;

-- IP 접근 권한 확인 사용 예제
SELECT CheckIPAccess('192.168.1.100') AS access_status;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. LAST_INSERT_ID()와 AUTO_INCREMENT - 웹 개발의 핵심&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LAST_INSERT_ID()&lt;/b&gt; 함수는 웹 애플리케이션에서 연관 데이터를 입력할 때 필수적인 함수입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제: 블로그 포스트와 태그 관리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 블로그 포스트 작성 과정
START TRANSACTION;

-- 1. 블로그 포스트 저장
INSERT INTO blog_posts (
    title, 
    content, 
    author_id, 
    status, 
    created_at
) VALUES (
    'MySQL 고유 함수 활용법',
    '이 글에서는 MySQL의 다양한 고유 함수들을 살펴봅니다...',
    456,
    'PUBLISHED',
    NOW()
);

-- 2. 방금 생성된 포스트 ID 가져오기
SET @post_id = LAST_INSERT_ID();

-- 3. 포스트 태그 연결
INSERT INTO post_tags (post_id, tag_name) VALUES
(@post_id, 'MySQL'),
(@post_id, 'Database'),
(@post_id, 'Tutorial'),
(@post_id, 'Programming');

-- 4. 포스트 통계 초기화
INSERT INTO post_statistics (
    post_id, 
    view_count, 
    like_count, 
    created_at
) VALUES (
    @post_id, 
    0, 
    0, 
    NOW()
);

COMMIT;

-- 생성된 포스트 정보 확인
SELECT p.id,
       p.title,
       p.author_id,
       GROUP_CONCAT(pt.tag_name ORDER BY pt.tag_name) AS tags,
       ps.view_count,
       ps.like_count
FROM blog_posts p
LEFT JOIN post_tags pt ON p.id = pt.post_id
LEFT JOIN post_statistics ps ON p.id = ps.post_id
WHERE p.id = @post_id
GROUP BY p.id;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. JSON 함수들 - 현대 웹 개발의 필수 도구&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 5.7부터 도입된 &lt;b&gt;JSON 관련 함수&lt;/b&gt;들은 NoSQL과 관계형 데이터베이스의 장점을 결합한 강력한 기능을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제: 사용자 설정 및 프로필 관리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 사용자 프로필 테이블 (JSON 활용)
CREATE TABLE user_profiles (
    user_id INT PRIMARY KEY,
    preferences JSON,
    profile_data JSON,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 사용자 프로필 데이터 저장
INSERT INTO user_profiles (user_id, preferences, profile_data) VALUES
(123, 
 JSON_OBJECT(
     'theme', 'dark',
     'language', 'ko',
     'notifications', JSON_OBJECT(
         'email', true,
         'push', false,
         'sms', true
     ),
     'timezone', 'Asia/Seoul'
 ),
 JSON_OBJECT(
     'displayName', '홍길동',
     'bio', 'MySQL 개발자',
     'skills', JSON_ARRAY('MySQL', 'PHP', 'JavaScript'),
     'social', JSON_OBJECT(
         'github', 'github.com/honggildong',
         'linkedin', 'linkedin.com/in/honggildong'
     )
 )
);

-- JSON 데이터 조회 및 활용
SELECT user_id,
       JSON_EXTRACT(profile_data, '$.displayName') AS display_name,
       JSON_EXTRACT(profile_data, '$.bio') AS bio,
       JSON_EXTRACT(preferences, '$.theme') AS theme,
       JSON_EXTRACT(preferences, '$.notifications.email') AS email_notifications,
       JSON_LENGTH(JSON_EXTRACT(profile_data, '$.skills')) AS skill_count
FROM user_profiles
WHERE user_id = 123;

-- 특정 스킬을 가진 사용자 검색
SELECT user_id,
       JSON_EXTRACT(profile_data, '$.displayName') AS display_name,
       JSON_EXTRACT(profile_data, '$.skills') AS skills
FROM user_profiles
WHERE JSON_CONTAINS(
    JSON_EXTRACT(profile_data, '$.skills'), 
    '&quot;MySQL&quot;'
);

-- 사용자 설정 업데이트
UPDATE user_profiles 
SET preferences = JSON_SET(
    preferences,
    '$.theme', 'light',
    '$.notifications.push', true
)
WHERE user_id = 123;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MySQL 고유 함수의 실무 가치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 고유 함수들은 웹 애플리케이션 개발에서 실질적이고 즉시 활용 가능한 도구들입니다. UUID() 함수는 분산 환경에서의 고유성을 보장하고, GROUP_CONCAT()은 복잡한 데이터 집계를 간단하게 만들며, 락 관련 함수들은 동시성 제어 문제를 우아하게 해결합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 웹 개발에서 자주 마주치는 IP 주소 처리, JSON 데이터 관리, 동시성 제어와 같은 실무 과제들을 MySQL의 고유 함수로 효율적으로 해결할 수 있습니다. 이러한 함수들을 적절히 활용하면 애플리케이션의 성능을 향상시키고, 코드의 복잡성을 줄이며, 개발 생산성을 크게 높일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 지속적인 발전과 함께 새로운 함수들이 계속 추가되고 있으므로, 최신 버전의 기능들을 주기적으로 확인하고 실무에 적용해보는 것이 중요합니다. 특히 JSON 관련 함수들은 현대 웹 개발의 트렌드를 반영한 강력한 기능이므로, 적극적으로 활용해볼 만합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[전문용어]&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;[UUID]&lt;/b&gt;: Universally Unique Identifier, 범용 고유 식별자로 전 세계적으로 유일한 값을 생성하는 표준&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[UUID_TO_BIN]&lt;/b&gt;: UUID 문자열을 16바이트 바이너리 형식으로 변환하는 MySQL 8.0 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[GROUP_CONCAT]&lt;/b&gt;: 그룹화된 데이터를 하나의 문자열로 연결하는 MySQL 전용 집계 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[GET_LOCK/RELEASE_LOCK]&lt;/b&gt;: MySQL의 사용자 정의 락 함수로 애플리케이션 레벨 동시성 제어에 사용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[INET_ATON/INET_NTOA]&lt;/b&gt;: IP 주소 문자열과 정수 간 변환을 수행하는 MySQL 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[LAST_INSERT_ID]&lt;/b&gt;: AUTO_INCREMENT로 생성된 마지막 ID 값을 반환하는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[JSON 함수]&lt;/b&gt;: MySQL 5.7부터 지원하는 JSON 데이터 타입 및 관련 처리 함수들&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MySql</category>
      <category>auto_increment</category>
      <category>group_concat</category>
      <category>ip 주소 처리</category>
      <category>json</category>
      <category>MYSQL</category>
      <category>UUID</category>
      <category>고유 식별자</category>
      <category>동시성 제어</category>
      <category>락 함수</category>
      <category>웹 개발</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/130</guid>
      <comments>https://devsite.tistory.com/entry/MySQL-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%9D%98-%EA%B3%A0%EC%9C%A0-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry130comment</comments>
      <pubDate>Sat, 24 May 2025 21:57:20 +0900</pubDate>
    </item>
    <item>
      <title>Oracle 데이터베이스의 고유 함수 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%9D%98-%EA%B3%A0%EC%9C%A0-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkbNYw/btsOaKWR2G5/teTNGabhqhfkEejCJivCD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkbNYw/btsOaKWR2G5/teTNGabhqhfkEejCJivCD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkbNYw/btsOaKWR2G5/teTNGabhqhfkEejCJivCD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkbNYw%2FbtsOaKWR2G5%2FteTNGabhqhfkEejCJivCD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;168&quot; data-filename=&quot;download.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 데이터베이스는 기업용 RDBMS의 대표주자로, 다른 데이터베이스 시스템에서는 찾아볼 수 없는 강력하고 독특한 함수들을 제공합니다. 이러한 고유 함수들은 복잡한 비즈니스 로직을 간결하게 처리하고, 대용량 데이터 처리 성능을 향상시키는 데 핵심적인 역할을 합니다. Oracle 개발자라면 반드시 알아야 할 핵심 함수들을 실제 예제와 함께 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. DECODE 함수 - Oracle의 대표적인 조건부 처리 함수&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DECODE 함수&lt;/b&gt;는 Oracle의 가장 상징적인 함수로, 프로그래밍 언어의 SWITCH-CASE 문과 유사한 조건부 로직을 SQL에서 간결하게 처리할 수 있게 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 문법과 동작 원리&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;DECODE(표현식, 비교값1, 결과값1, 비교값2, 결과값2, ..., 기본값)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE는 첫 번째 매개변수의 값을 순차적으로 비교값들과 비교하여, 일치하는 경우 해당하는 결과값을 반환합니다. 어떤 비교값과도 일치하지 않으면 기본값을 반환합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 고객 등급 변환&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT customer_name,
       DECODE(grade_code, 
              'A', '프리미엄',
              'B', '골드',
              'C', '실버',
              'D', '브론즈',
              '일반회원') AS customer_grade,
       DECODE(grade_code,
              'A', 0.15,
              'B', 0.10,
              'C', 0.05,
              'D', 0.02,
              0) AS discount_rate
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 중첩 DECODE를 활용한 복잡한 조건 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT employee_name,
       salary,
       DECODE(department_id,
              10, DECODE(job_id, 'MANAGER', salary * 1.2, salary * 1.1),
              20, DECODE(job_id, 'MANAGER', salary * 1.25, salary * 1.15),
              30, salary * 1.05,
              salary) AS adjusted_salary
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제는 부서별, 직급별로 다른 급여 조정률을 적용하는 복잡한 비즈니스 로직을 간결하게 구현한 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. SYS_GUID() - 글로벌 고유 식별자 생성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SYS_GUID()&lt;/b&gt; 함수는 전 세계적으로 고유한 16바이트 식별자를 생성하는 Oracle 전용 함수입니다. 분산 환경에서 데이터 중복을 방지하고 고유성을 보장하는 데 매우 유용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 사용법&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT SYS_GUID() FROM dual;
-- 결과 예: A1B2C3D4E5F67890123456789ABCDEF0

-- 테이블에서 활용
CREATE TABLE orders (
    order_id RAW(16) DEFAULT SYS_GUID(),
    customer_id NUMBER,
    order_date DATE DEFAULT SYSDATE,
    amount NUMBER(10,2)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 분산 환경에서의 고유 키 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;INSERT INTO order_items (
    item_id,
    order_id,
    product_id,
    quantity,
    unit_price
) VALUES (
    SYS_GUID(),
    '12345',
    'PROD001',
    2,
    15000
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 데이터 통합 시 중복 방지&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 여러 지점의 데이터를 통합할 때 고유성 보장
INSERT INTO master_transactions 
SELECT SYS_GUID() AS transaction_id,
       branch_id,
       transaction_date,
       amount,
       customer_id
FROM branch_transactions
WHERE sync_status = 'PENDING';
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. CONNECT BY - 계층적 쿼리의 강력한 도구&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CONNECT BY&lt;/b&gt;는 Oracle에서 트리 구조 데이터를 처리하기 위한 전용 구문으로, 조직도, 카테고리 계층, 댓글 구조 등 계층적 데이터를 쉽게 탐색할 수 있게 해줍니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 문법&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT [컬럼명]
FROM [테이블명]
START WITH [시작조건]
CONNECT BY [PRIOR] [계층조건]
ORDER SIBLINGS BY [정렬조건];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 조직도 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT LEVEL,
       LPAD(' ', (LEVEL-1)*2) || employee_name AS org_chart,
       employee_id,
       manager_id,
       job_title
FROM employees
START WITH manager_id IS NULL
CONNECT BY PRIOR employee_id = manager_id
ORDER SIBLINGS BY employee_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 제품 카테고리 계층 구조&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT LEVEL,
       SYS_CONNECT_BY_PATH(category_name, ' &amp;gt; ') AS category_path,
       category_id,
       parent_category_id
FROM product_categories
START WITH parent_category_id IS NULL
CONNECT BY PRIOR category_id = parent_category_id
ORDER SIBLINGS BY display_order;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 &quot;전자제품 &amp;gt; 컴퓨터 &amp;gt; 노트북&quot;과 같은 카테고리 경로를 자동으로 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. NVL과 NVL2 - NULL 처리의 유연한 해결책&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 &lt;b&gt;NVL&lt;/b&gt;과 &lt;b&gt;NVL2&lt;/b&gt; 함수는 NULL 값을 효과적으로 처리하는 강력한 도구입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;함수별 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;NVL(expr1, expr2)&lt;/b&gt;: expr1이 NULL이면 expr2를 반환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NVL2(expr1, expr2, expr3)&lt;/b&gt;: expr1이 NULL이 아니면 expr2를, NULL이면 expr3를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 기본값 설정과 계산&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT employee_name,
       salary,
       NVL(commission_pct, 0) AS commission_pct,
       salary + (salary * NVL(commission_pct, 0)) AS total_compensation,
       NVL2(commission_pct, 
            'Commission Based', 
            'Fixed Salary') AS compensation_type
FROM employees
ORDER BY total_compensation DESC;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 연락처 정보 통합 표시&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT customer_name,
       NVL2(mobile_phone, 
            'Mobile: ' || mobile_phone,
            NVL2(home_phone, 
                 'Home: ' || home_phone,
                 'No Phone Available')) AS contact_info
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. ROWNUM과 ROWID - Oracle의 특별한 의사 열&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ROWNUM&lt;/b&gt;과 &lt;b&gt;ROWID&lt;/b&gt;는 Oracle이 제공하는 특별한 의사 열(Pseudocolumn)로, 각각 고유한 용도와 특성을 가지고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ROWNUM 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 상위 N개 레코드 조회&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 급여 상위 10명 조회
SELECT *
FROM (
    SELECT employee_name, salary, department_id
    FROM employees
    ORDER BY salary DESC
)
WHERE ROWNUM &amp;lt;= 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 페이징 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 11번째부터 20번째 레코드 조회 (페이지 2)
SELECT *
FROM (
    SELECT employee_name, salary, ROWNUM rn
    FROM (
        SELECT employee_name, salary
        FROM employees
        ORDER BY salary DESC
    )
    WHERE ROWNUM &amp;lt;= 20
)
WHERE rn &amp;gt; 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ROWID 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제: 중복 데이터 제거&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 중복된 고객 정보 중 가장 최근 레코드만 유지
DELETE FROM customers c1
WHERE ROWID &amp;gt; (
    SELECT MIN(ROWID)
    FROM customers c2
    WHERE c1.customer_email = c2.customer_email
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. LISTAGG - 그룹 데이터의 문자열 연결&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LISTAGG&lt;/b&gt; 함수는 Oracle 11g부터 도입된 함수로, 그룹화된 값들을 하나의 문자열로 연결하는 강력한 기능을 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 1: 부서별 직원 목록 생성&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT department_name,
       LISTAGG(employee_name, ', ') 
       WITHIN GROUP (ORDER BY employee_name) AS employee_list,
       COUNT(*) AS employee_count
FROM employees e
JOIN departments d ON e.department_id = d.department_id
GROUP BY department_name
ORDER BY department_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 2: 고객별 주문 상품 목록&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT c.customer_name,
       LISTAGG(p.product_name, ' | ') 
       WITHIN GROUP (ORDER BY oi.order_date DESC) AS recent_orders
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
WHERE o.order_date &amp;gt;= SYSDATE - 30  -- 최근 30일
GROUP BY c.customer_id, c.customer_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Oracle 고유 함수의 활용 가치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 고유 함수들은 단순히 다른 데이터베이스와의 차별화 요소가 아닙니다. 이들은 복잡한 비즈니스 로직을 간결하게 표현하고, 성능을 최적화하며, 개발 생산성을 크게 향상시키는 실질적인 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 DECODE의 간결한 조건 처리, SYS_GUID()의 분산 환경 지원, CONNECT BY의 계층적 데이터 처리 능력은 Oracle을 선택하는 주요 이유 중 하나입니다. 이러한 함수들을 적절히 활용하면 더 효율적이고 읽기 쉬운 SQL 코드를 작성할 수 있으며, 복잡한 요구사항도 우아하게 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 다른 데이터베이스로의 이식성을 고려해야 하는 환경이라면, 표준 SQL과 Oracle 고유 함수 간의 균형을 잘 맞춰서 사용하는 것이 중요합니다. 각 함수의 특성과 제약사항을 정확히 이해하고 적절한 상황에서 활용한다면, Oracle 데이터베이스의 진정한 파워를 경험할 수 있을 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[전문용어]&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;[DECODE]&lt;/b&gt;: Oracle의 조건부 함수로, 입력값에 따라 다른 결과를 반환하는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[SYS_GUID]&lt;/b&gt;: Oracle에서 글로벌 고유 식별자(16바이트 RAW)를 생성하는 함수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[CONNECT BY]&lt;/b&gt;: Oracle의 계층적 쿼리 구문으로 트리 구조 데이터를 처리하는 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[NVL/NVL2]&lt;/b&gt;: Oracle의 NULL 처리 함수로, NULL 값을 다른 값으로 대체하는 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[ROWNUM]&lt;/b&gt;: Oracle의 의사 열로, 쿼리 결과에 순번을 부여하는 기능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[ROWID]&lt;/b&gt;: Oracle의 의사 열로, 각 행의 물리적 저장 위치를 나타내는 고유 식별자&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[LISTAGG]&lt;/b&gt;: Oracle 11g 이상에서 제공되는 함수로, 그룹화된 값들을 문자열로 연결하는 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>connect by</category>
      <category>decode</category>
      <category>listagg</category>
      <category>nvl</category>
      <category>nvl2</category>
      <category>Oracle</category>
      <category>ROWID</category>
      <category>ROWNUM</category>
      <category>sys_guid</category>
      <category>계층적 쿼리</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/129</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4%EC%9D%98-%EA%B3%A0%EC%9C%A0-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry129comment</comments>
      <pubDate>Sat, 24 May 2025 21:53:20 +0900</pubDate>
    </item>
    <item>
      <title>오라클 조인과 ANSI JOIN 차이점</title>
      <link>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%EC%A1%B0%EC%9D%B8%EA%B3%BC-ANSI-JOIN-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;oracle-logo-16x9_1920-1080.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVYlmf/btsN6tlpez7/vjRKCkZPda8GvzEHs9fh9k/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVYlmf/btsN6tlpez7/vjRKCkZPda8GvzEHs9fh9k/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVYlmf/btsN6tlpez7/vjRKCkZPda8GvzEHs9fh9k/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVYlmf%2FbtsN6tlpez7%2FvjRKCkZPda8GvzEHs9fh9k%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;327&quot; height=&quot;184&quot; data-filename=&quot;oracle-logo-16x9_1920-1080.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문법적 차이&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오라클 조인(Oracle 전통적 조인)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WHERE 절에서 조인 조건을 명시&lt;/li&gt;
&lt;li&gt;Oracle 데이터베이스에서 전통적으로 사용되던 방식&lt;/li&gt;
&lt;li&gt;조인 조건과 필터링 조건이 모두 WHERE 절에 있어 구분이 쉽지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ANSI JOIN&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SQL 표준(ANSI SQL)을 따르는 조인 문법&lt;/li&gt;
&lt;li&gt;FROM 절에서 JOIN 키워드를 사용하여 조인 조건을 명시&lt;/li&gt;
&lt;li&gt;ON 절을 통해 조인 조건을 명확히 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 예시 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;내부 조인(INNER JOIN)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오라클 조인 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.employee_id, e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id
AND e.hire_date &amp;gt; TO_DATE('01-JAN-2020', 'DD-MON-YYYY');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ANSI JOIN 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.employee_id, e.last_name, d.department_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id
WHERE e.hire_date &amp;gt; TO_DATE('01-JAN-2020', 'DD-MON-YYYY');&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 조인(OUTER JOIN)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오라클 조인 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.employee_id, e.last_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id(+);  -- 오른쪽 외부 조인(RIGHT OUTER JOIN)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ANSI JOIN 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.employee_id, e.last_name, d.department_name
FROM employees e
LEFT OUTER JOIN departments d ON e.department_id = d.department_id;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다중 조인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;오라클 조인 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.employee_id, e.last_name, d.department_name, l.city
FROM employees e, departments d, locations l
WHERE e.department_id = d.department_id
AND d.location_id = l.location_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ANSI JOIN 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT e.employee_id, e.last_name, d.department_name, l.city
FROM employees e
INNER JOIN departments d ON e.department_id = d.department_id
INNER JOIN locations l ON d.location_id = l.location_id;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 주요 장단점 비교&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오라클 조인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span class=&quot;advantage&quot;&gt;Oracle 데이터베이스에 최적화된 성능을 낼 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;advantage&quot;&gt;간단한 조인에서는 코드가 더 간결할 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span class=&quot;disadvantage&quot;&gt;조인 조건과 필터링 조건이 모두 WHERE 절에 있어 구분이 어려움&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;disadvantage&quot;&gt;복잡한 다중 조인에서 가독성이 떨어짐&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;disadvantage&quot;&gt;데이터베이스에 종속적인 문법&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ANSI JOIN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span class=&quot;advantage&quot;&gt;SQL 표준을 따르므로 다양한 DBMS에서 호환 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;advantage&quot;&gt;조인 조건(ON)과 필터링 조건(WHERE)이 명확히 구분됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;advantage&quot;&gt;복잡한 다중 조인에서 가독성이 좋음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;advantage&quot;&gt;다양한 조인 유형(INNER, LEFT, RIGHT, FULL, CROSS 등)을 명시적으로 표현&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span class=&quot;disadvantage&quot;&gt;문법이 좀 더 길어질 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;disadvantage&quot;&gt;일부 레거시 시스템에서는 지원이 제한적일 수 있음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;recommendation&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 권장 사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 SQL 개발에서는 다음과 같은 이유로 ANSI JOIN 사용을 권장합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;표준 준수로 인한 높은 호환성&lt;/li&gt;
&lt;li&gt;조인 의도가 명확히 드러나는 가독성&lt;/li&gt;
&lt;li&gt;다양한 조인 유형을 명시적으로 표현 가능&lt;/li&gt;
&lt;li&gt;조인 조건과 필터 조건의 명확한 구분&lt;/li&gt;
&lt;li&gt;유지보수 용이성&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description>
      <category>DB/Oracle</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/128</guid>
      <comments>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%EC%A1%B0%EC%9D%B8%EA%B3%BC-ANSI-JOIN-%EC%B0%A8%EC%9D%B4%EC%A0%90#entry128comment</comments>
      <pubDate>Tue, 20 May 2025 21:50:11 +0900</pubDate>
    </item>
    <item>
      <title>MS SQL Server에서 Oracle DECODE 대체하기: 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/MS-SQL-Server%EC%97%90%EC%84%9C-Oracle-DECODE-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Microsoft SQL Server(MSSQL)에는 Oracle의 DECODE 함수와 동일한 기능이 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 MSSQL은 CASE 문과 다양한 조건부 함수들을 통해 DECODE의 기능을 효과적으로 구현할 수 있습니다. 이러한 대체 방법을 활용하면 Oracle에서 MSSQL로의 마이그레이션 과정에서 코드 변환을 보다 쉽게 수행할 수 있습니다. 왜 대기업들이 Oracle에서 MSSQL로 전환하는 추세가 증가하고 있을까요? 서로 다른 데이터베이스 시스템 간 코드 호환성이 비즈니스 연속성에 어떤 영향을 미칠까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. MS SQL Server의 CASE 문으로 DECODE 대체하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 DECODE 함수와 마찬가지로, MS SQL Server의 &lt;b&gt;CASE 문은 조건부 로직을 구현하는 가장 기본적인 방법&lt;/b&gt;입니다. CASE 문은 표준 SQL의 일부이므로, Oracle, MySQL, MS SQL Server 모두에서 사용할 수 있어 이식성이 뛰어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 문법 비교:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- Oracle의 DECODE 문법
SELECT DECODE(column_name, value1, result1, value2, result2, default_result)
FROM table_name;

-- MS SQL Server의 CASE 문법
SELECT 
  CASE column_name
    WHEN value1 THEN result1
    WHEN value2 THEN result2
    ELSE default_result
  END AS result_column
FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용 예시, 고객 등급 코드를 텍스트로 변환하는 경우:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Oracle DECODE 버전
SELECT 
  customer_name,
  DECODE(status_code, 'A', '활성', 'I', '비활성', 'P', '보류중', '미정') AS status
FROM customers;

-- MS SQL Server CASE 버전
SELECT 
  customer_name,
  CASE status_code
    WHEN 'A' THEN '활성'
    WHEN 'I' THEN '비활성'
    WHEN 'P' THEN '보류중'
    ELSE '미정'
  END AS status
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL의 CASE 문은 두 가지 형식을 지원합니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 단순 CASE 문&lt;/b&gt; - 위 예시처럼 등식 비교만 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) 검색 CASE 문&lt;/b&gt; - 다양한 조건식을 사용할 수 있어 더 유연합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 검색 CASE 문 예시
SELECT 
  product_name,
  CASE 
    WHEN price &amp;lt; 50 THEN '저가'
    WHEN price BETWEEN 50 AND 200 THEN '중가'
    WHEN price &amp;gt; 200 THEN '고가'
    ELSE '가격 미정'
  END AS price_category
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. MS SQL Server의 특별한 조건부 함수들&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server는 CASE 문 외에도 &lt;b&gt;Oracle의 DECODE 기능을 대체할 수 있는 여러 고유 함수&lt;/b&gt;를 제공합니다. 이러한 함수들은 특정 상황에서 CASE보다 더 간결한 코드를 작성할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) IIF 함수&lt;/b&gt; (SQL Server 2012 이상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IIF는 삼항 연산자와 유사하게 작동하며, 단일 조건에 따라 두 값 중 하나를 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 문법: IIF(condition, true_value, false_value)
SELECT 
  product_name,
  IIF(in_stock &amp;gt; 0, '재고 있음', '품절') AS stock_status
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) CHOOSE 함수&lt;/b&gt; (SQL Server 2012 이상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CHOOSE는 인덱스 값(1부터 시작)에 기반하여 목록에서 값을 선택합니다. DECODE의 특정 패턴을 매우 간결하게 대체할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 문법: CHOOSE(index, value1, value2, ...)
SELECT 
  order_id,
  CHOOSE(priority, '낮음', '중간', '높음', '긴급') AS priority_level
FROM orders;

-- 이는 다음 CASE 문과 동일합니다
SELECT 
  order_id,
  CASE priority
    WHEN 1 THEN '낮음'
    WHEN 2 THEN '중간'
    WHEN 3 THEN '높음'
    WHEN 4 THEN '긴급'
    ELSE NULL
  END AS priority_level
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) COALESCE 함수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL이 아닌 첫 번째 값을 반환합니다. 여러 열에서 유효한 값을 찾는 경우에 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;-- 문법: COALESCE(expr1, expr2, ..., exprN)
SELECT 
  customer_name,
  COALESCE(mobile_phone, home_phone, email, '연락처 없음') AS contact
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) ISNULL 함수&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 IFNULL과 유사하게, NULL 값을 대체 값으로 변환합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 문법: ISNULL(expression, replacement)
SELECT 
  employee_name,
  ISNULL(commission, 0) AS commission
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 복잡한 DECODE 패턴 구현하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서 &lt;b&gt;중첩 DECODE 함수로 구현된 복잡한 로직&lt;/b&gt;은 MS SQL Server에서 여러 방법으로 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) 중첩 CASE 문 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Oracle 중첩 DECODE
SELECT
  DECODE(gender, 
    'M', DECODE(age, 
           &amp;lt; 20, '남성 청소년',
           BETWEEN 20 AND 60, '남성 성인',
           '남성 노년층'),
    'F', DECODE(age,
           &amp;lt; 20, '여성 청소년',
           BETWEEN 20 AND 60, '여성 성인',
           '여성 노년층'),
    '미지정') AS demographic
FROM customers;

-- MS SQL Server 중첩 CASE
SELECT
  CASE gender
    WHEN 'M' THEN 
      CASE 
        WHEN age &amp;lt; 20 THEN '남성 청소년'
        WHEN age BETWEEN 20 AND 60 THEN '남성 성인'
        ELSE '남성 노년층'
      END
    WHEN 'F' THEN
      CASE 
        WHEN age &amp;lt; 20 THEN '여성 청소년'
        WHEN age BETWEEN 20 AND 60 THEN '여성 성인'
        ELSE '여성 노년층'
      END
    ELSE '미지정'
  END AS demographic
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) IIF 함수 중첩 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 중첩 조건의 경우 IIF 함수를 중첩하여 더 간결한 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  IIF(gender = 'M',
    IIF(age &amp;lt; 20, '남성 청소년',
      IIF(age BETWEEN 20 AND 60, '남성 성인', '남성 노년층')),
    IIF(gender = 'F',
      IIF(age &amp;lt; 20, '여성 청소년',
        IIF(age BETWEEN 20 AND 60, '여성 성인', '여성 노년층')),
      '미지정')) AS demographic
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 서비스 회사 G사는 Oracle에서 MS SQL Server로 마이그레이션하면서 &lt;b&gt;800개 이상의 DECODE 함수를 변환&lt;/b&gt;했습니다. 이 과정에서 패턴별 변환 가이드를 개발하여 개발자들이 최적의 MSSQL 대체 함수를 선택할 수 있도록 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. MS SQL Server의 고급 기능 활용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server 2012 이상 버전에서는 &lt;b&gt;다양한 고급 기능&lt;/b&gt;을 활용하여 DECODE 함수의 기능을 더 효율적으로 구현할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1) 윈도우 함수와 CASE 조합&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  department,
  employee_name,
  salary,
  CASE 
    WHEN salary &amp;gt; AVG(salary) OVER(PARTITION BY department) THEN '부서 평균보다 높음'
    WHEN salary = AVG(salary) OVER(PARTITION BY department) THEN '부서 평균과 동일'
    ELSE '부서 평균보다 낮음'
  END AS salary_comparison
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2) JSON 기능과 조건부 로직 조합&lt;/b&gt; (SQL Server 2016 이상)&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  order_id,
  CASE 
    WHEN JSON_VALUE(order_data, '$.status') = 'shipped' THEN '배송완료'
    WHEN JSON_VALUE(order_data, '$.status') = 'processing' THEN '처리중'
    ELSE '주문접수'
  END AS order_status
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3) STRING_SPLIT과 조건부 로직 조합&lt;/b&gt; (SQL Server 2016 이상)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉼표로 구분된 값 목록이 있는 경우, STRING_SPLIT 함수와 조건부 로직을 조합하여 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  product_id,
  product_name,
  CASE 
    WHEN EXISTS (
      SELECT 1 
      FROM STRING_SPLIT(categories, ',') 
      WHERE value = 'electronic'
    ) THEN '전자제품 포함'
    ELSE '전자제품 아님'
  END AS electronic_category
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. MS SQL Server와 Oracle 간 성능 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 DECODE와 MS SQL Server의 CASE 문 간에는 &lt;b&gt;성능 차이가 있을 수 있습니다&lt;/b&gt;. 일반적으로 두 기능 모두 잘 최적화되어 있지만, 특정 시나리오에서는 고려해야 할 성능 차이가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 성능 고려사항:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CASE 문의 평가 순서가 중요합니다. MS SQL Server에서는 &lt;b&gt;첫 번째 TRUE 조건을 찾으면 나머지 조건은 평가하지 않습니다&lt;/b&gt;. 따라서 가장 자주 매칭되는 조건을 먼저 배치하는 것이 성능에 도움이 됩니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 성능 최적화를 위한 조건 배치
SELECT
  transaction_id,
  CASE 
    WHEN type_code = 'P' THEN '결제' -- 가장 흔한 경우
    WHEN type_code = 'R' THEN '환불'
    WHEN type_code = 'A' THEN '조정'
    ELSE '기타'
  END AS transaction_type
FROM transactions;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 성능 컨설팅 회사 H사의 분석에 따르면, &lt;b&gt;대량의 데이터를 처리할 때 CASE 문보다 CHOOSE 함수가 약 10-15% 빠른 성능&lt;/b&gt;을 보였습니다. 그러나 이는 CHOOSE의 제한된 사용 사례(1부터 시작하는 인덱스 기반)에서만 적용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스된 열에 CASE, IIF 등을 적용할 때 주의해야 합니다. 인덱스 사용을 방해할 수 있기 때문입니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 인덱스 사용이 어려울 수 있는 쿼리
SELECT *
FROM orders
WHERE CASE 
      WHEN customer_type = 'VIP' THEN total_amount &amp;gt; 1000
      ELSE total_amount &amp;gt; 5000
      END;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 실무자를 위한 마이그레이션 팁&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서 MS SQL Server로 마이그레이션할 때 DECODE 함수 처리를 위한 &lt;b&gt;실용적인 팁&lt;/b&gt;을 소개합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1) DECODE 패턴별 최적의 MSSQL 대체 방법&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;-- 단순 값 매핑 패턴: CHOOSE 함수 사용 (값이 1부터 순차적일 때)
-- Oracle: DECODE(status, 1, '대기', 2, '진행', 3, '완료', '알 수 없음')
-- MSSQL: CHOOSE(status, '대기', '진행', '완료') 또는 상태값이 NULL이면 ISNULL(CHOOSE(status, '대기', '진행', '완료'), '알 수 없음')

-- NULL 처리 패턴: ISNULL 함수 사용
-- Oracle: DECODE(commission, NULL, 0, commission)
-- MSSQL: ISNULL(commission, 0)

-- 복잡한 조건 패턴: 검색 CASE 문 사용
-- Oracle: DECODE(SIGN(score - 90), 1, 'A', DECODE(SIGN(score - 80), 1, 'B', DECODE(SIGN(score - 70), 1, 'C', 'D')))
-- MSSQL: CASE WHEN score &amp;gt;= 90 THEN 'A' WHEN score &amp;gt;= 80 THEN 'B' WHEN score &amp;gt;= 70 THEN 'C' ELSE 'D' END
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2) 자동화 도구 활용&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 마이그레이션 프로젝트의 경우, Oracle에서 MS SQL Server로 코드를 자동으로 변환해주는 도구를 활용하면 효율적입니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Microsoft SQL Server Migration Assistant for Oracle&lt;/li&gt;
&lt;li&gt;AWS Schema Conversion Tool&lt;/li&gt;
&lt;li&gt;SqlDBM 등의 데이터베이스 모델링 도구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 도구들은 DECODE 함수를 포함한 Oracle 구문을 MS SQL Server 구문으로 자동 변환해주는 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의료 정보 시스템 회사 I사는 &lt;b&gt;3백만 라인 이상의 Oracle PL/SQL 코드&lt;/b&gt;를 MS SQL Server로 마이그레이션했습니다. 이 과정에서 자동화 도구와 함께 자체 개발한 코드 변환기를 사용하여 DECODE 함수의 95% 이상을 성공적으로 변환했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리: 데이터베이스 플랫폼 간 코드 이식성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS SQL Server는 Oracle의 DECODE 함수를 직접적으로 제공하지 않지만, CASE 문과 다양한 조건부 함수들(IIF, CHOOSE, COALESCE, ISNULL)을 통해 동일한 기능을 효과적으로 구현할 수 있습니다. 특히 SQL Server 2012 이상 버전에서 도입된 IIF와 CHOOSE 함수는 많은 DECODE 사용 사례를 더욱 간결하게 대체할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이브리드 클라우드 환경과 다중 데이터베이스 아키텍처가 보편화되는 현대 IT 환경에서는 데이터베이스 플랫폼 간 코드 이식성이 더욱 중요해지고 있습니다. 표준 SQL을 사용하고 데이터베이스 고유 함수의 사용을 최소화하면, 미래의 마이그레이션 작업을 보다 쉽게 수행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MSSQL</category>
      <category>case 문</category>
      <category>choose</category>
      <category>coalesce</category>
      <category>decode</category>
      <category>IIF</category>
      <category>ISNULL</category>
      <category>MS SQL Server</category>
      <category>Oracle</category>
      <category>sql 표준</category>
      <category>데이터베이스 마이그레이션</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/127</guid>
      <comments>https://devsite.tistory.com/entry/MS-SQL-Server%EC%97%90%EC%84%9C-Oracle-DECODE-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry127comment</comments>
      <pubDate>Tue, 20 May 2025 21:29:07 +0900</pubDate>
    </item>
    <item>
      <title>MySQL에서 Oracle DECODE 대체하기: 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/MySQL%EC%97%90%EC%84%9C-Oracle-DECODE-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;vkGpBcmks1_NcJW0HUFa6jlwlM6h11B-8nxRRX4bYC703H4nLo7j4dQdRCC32gz8Q-BqRcAnQgFSXMjB8jPohg.svg&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7gxF4/btsN5pKRjox/50l1wai7bQfaGQfwBJ2HB0/tfile.svg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7gxF4/btsN5pKRjox/50l1wai7bQfaGQfwBJ2HB0/tfile.svg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7gxF4/btsN5pKRjox/50l1wai7bQfaGQfwBJ2HB0/tfile.svg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7gxF4%2FbtsN5pKRjox%2F50l1wai7bQfaGQfwBJ2HB0%2Ftfile.svg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;281&quot; height=&quot;191&quot; data-filename=&quot;vkGpBcmks1_NcJW0HUFa6jlwlM6h11B-8nxRRX4bYC703H4nLo7j4dQdRCC32gz8Q-BqRcAnQgFSXMjB8jPohg.svg&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL에는 Oracle의 DECODE 함수와 동일한 기능이 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 MySQL에서는 CASE 문과 여러 조건부 함수를 사용하여 DECODE와 동일한 기능을 구현할 수 있습니다. 이러한 대체 방법은 데이터베이스 간 이식성을 높이면서도 Oracle DECODE의 핵심 기능을 그대로 활용할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL과 Oracle 간 코드 마이그레이션을 계획 중이신가요?&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Oracle DECODE와 MySQL CASE 문 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 DECODE 함수는 조건부 로직을 간결하게, 그리고 특정 패턴으로 처리하도록 설계되었습니다. MySQL에서는 &lt;b&gt;표준 SQL의 CASE 문을 사용하여 유사한 기능을 구현&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 문법 비교:&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- Oracle의 DECODE 문법
SELECT DECODE(column_name, value1, result1, value2, result2, default_result)
FROM table_name;

-- MySQL의 CASE 문법
SELECT 
  CASE column_name
    WHEN value1 THEN result1
    WHEN value2 THEN result2
    ELSE default_result
  END AS result_column
FROM table_name;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용 예시를 살펴보면, 부서 코드를 부서명으로 변환하는 쿼리의 경우:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Oracle DECODE 버전
SELECT 
  employee_name,
  DECODE(dept_code, 10, '인사부', 20, '재무부', 30, '마케팅부', '기타 부서') AS department
FROM employees;

-- MySQL CASE 버전
SELECT 
  employee_name,
  CASE dept_code
    WHEN 10 THEN '인사부'
    WHEN 20 THEN '재무부'
    WHEN 30 THEN '마케팅부'
    ELSE '기타 부서'
  END AS department
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. MySQL의 조건부 함수 활용하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 CASE 외에도 &lt;b&gt;여러 조건부 함수&lt;/b&gt;를 제공하여 DECODE의 다양한 사용 사례를 대체할 수 있습니다. 이러한 함수들은 특정 상황에서 CASE보다 더 간결한 코드를 작성할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 조건부 함수들:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1) IF 함수&lt;/b&gt; - 단일 조건에 따라 두 가지 결과 중 하나를 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 문법: IF(condition, value_if_true, value_if_false)
SELECT 
  product_name,
  IF(price &amp;gt; 100, '고가', '저가') AS price_category
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2) IFNULL 함수&lt;/b&gt; - NULL 값을 대체 값으로 변환합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 문법: IFNULL(expression, replacement_value)
SELECT 
  customer_name,
  IFNULL(phone_number, '연락처 없음') AS contact
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3) NULLIF 함수&lt;/b&gt; - 두 표현식이 동일하면 NULL을 반환하고, 그렇지 않으면 첫 번째 표현식을 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;-- 문법: NULLIF(expr1, expr2)
SELECT 
  order_id,
  NULLIF(shipping_address, billing_address) AS different_shipping_address
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4) COALESCE 함수&lt;/b&gt; - 여러 표현식 중 NULL이 아닌 첫 번째 값을 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;-- 문법: COALESCE(value1, value2, ..., valueN)
SELECT 
  employee_name,
  COALESCE(mobile_phone, office_phone, email, '연락 불가') AS contact_method
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 복잡한 DECODE 로직 대체하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서 &lt;b&gt;중첩된 DECODE 함수&lt;/b&gt;를 사용하는 경우, MySQL에서는 중첩된 CASE 문이나 다른 조건부 함수의 조합으로 이를 대체할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 변환 로직 예시:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Oracle 중첩 DECODE
SELECT
  DECODE(payment_type, 
    'CARD', DECODE(card_type, 
             'VISA', '비자카드',
             'MC', '마스터카드',
             '기타카드'),
    'BANK', '계좌이체',
    'CASH', '현금',
    '기타 결제수단') AS payment_method
FROM payments;

-- MySQL 중첩 CASE
SELECT
  CASE payment_type
    WHEN 'CARD' THEN 
      CASE card_type
        WHEN 'VISA' THEN '비자카드'
        WHEN 'MC' THEN '마스터카드'
        ELSE '기타카드'
      END
    WHEN 'BANK' THEN '계좌이체'
    WHEN 'CASH' THEN '현금'
    ELSE '기타 결제수단'
  END AS payment_method
FROM payments;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 금융 회사 D사는 Oracle에서 MySQL로 마이그레이션하면서 &lt;b&gt;1,500여 개의 DECODE 함수를 CASE 문으로 변환&lt;/b&gt;했습니다. 이 과정에서 자동화 스크립트를 활용하여 변환 시간을 크게 단축했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 성능 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 DECODE와 MySQL의 CASE 문 간에는 &lt;b&gt;성능 차이가 있을 수 있습니다&lt;/b&gt;. 일반적으로 두 기능 모두 잘 최적화되어 있지만, 데이터베이스 버전과 특정 사용 사례에 따라 성능이 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 성능 고려사항:&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 CASE 문은 표현식이 많을수록 평가할 조건이 많아져 &lt;b&gt;성능에 영향을 줄 수 있습니다&lt;/b&gt;. 가능하면 CASE 문의 조건을 최소화하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 컨설팅 회사 E사의 벤치마크 테스트에 따르면, 단순 매핑 작업에서는 CASE 문과 IF 함수 간의 성능 차이가 미미했지만, &lt;b&gt;복잡한 중첩 조건에서는 CASE 문의 가독성이 더 높았습니다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스된 열에 CASE나 IF를 적용할 때도 주의가 필요합니다. 인덱스 사용에 영향을 줄 수 있기 때문입니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 인덱스 활용이 어려울 수 있는 쿼리
SELECT *
FROM products
WHERE CASE category
      WHEN 'A' THEN price &amp;gt; 100
      WHEN 'B' THEN price &amp;gt; 200
      ELSE price &amp;gt; 300
      END;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. MySQL 8.0 이상의 개선된 기능&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 이상 버전에서는 이전 버전에 비해 &lt;b&gt;조건부 로직 처리가 개선&lt;/b&gt;되었습니다. 특히 JSON 데이터 처리와 함께 사용할 수 있는 새로운 함수들이 추가되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0의 JSON 기능과 CASE를 조합한 예시:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  order_id,
  CASE 
    WHEN JSON_EXTRACT(order_data, '$.status') = 'shipped' THEN '배송완료'
    WHEN JSON_EXTRACT(order_data, '$.status') = 'processing' THEN '처리중'
    ELSE '주문접수'
  END AS order_status
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 MySQL 8.0에서는 &lt;b&gt;윈도우 함수와 함께 CASE를 사용&lt;/b&gt;하여 더 복잡한 분석 쿼리를 작성할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  department,
  employee_name,
  salary,
  CASE 
    WHEN salary &amp;gt; AVG(salary) OVER(PARTITION BY department) THEN '평균 이상'
    ELSE '평균 이하'
  END AS salary_category
FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 실무자를 위한 마이그레이션 팁&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서 MySQL로 마이그레이션할 때 DECODE 함수 처리를 위한 &lt;b&gt;실용적인 팁&lt;/b&gt;을 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화 도구를 활용하세요. 대규모 코드베이스의 경우, DECODE를 CASE로 자동 변환하는 스크립트를 사용하면 시간을 크게 절약할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특수한 DECODE 사용 패턴을 식별하고 MySQL에서 최적의 대체 방법을 선택하세요:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Oracle NULL 처리 DECODE
SELECT DECODE(commission, NULL, 0, commission) FROM employees;

-- MySQL 대체 방법: IFNULL 사용 (더 간결)
SELECT IFNULL(commission, 0) FROM employees;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대형 전자상거래 회사 F사는 &lt;b&gt;DECODE를 사용하는 500개 이상의 저장 프로시저&lt;/b&gt;를 MySQL로 마이그레이션했습니다. 이 과정에서 패턴별 변환 가이드를 만들어 팀원들이 일관된 방식으로 코드를 변환할 수 있게 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리: 데이터베이스 간 조건부 로직의 미래&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL은 Oracle의 DECODE 함수를 직접적으로 제공하지 않지만, CASE 문과 다양한 조건부 함수들을 통해 동일한 기능을 효과적으로 구현할 수 있습니다. 이러한 표준 SQL 구문을 사용하면 데이터베이스 간 이식성도 향상됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라우드 기반 데이터베이스 환경이 확산되고 다중 데이터베이스 아키텍처가 보편화됨에 따라, 특정 데이터베이스에 종속된 함수보다는 표준 SQL을 사용하는 접근 방식이 더욱 중요해질 것입니다. 이러한 맥락에서 MySQL의 CASE 문과 조건부 함수들은 Oracle DECODE의 기능을 충분히 대체할 수 있는 강력한 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/MySql</category>
      <category>case 문</category>
      <category>coalesce</category>
      <category>decode</category>
      <category>iF</category>
      <category>ifnull</category>
      <category>MYSQL</category>
      <category>NULLIF</category>
      <category>Oracle</category>
      <category>데이터베이스 마이그레이션</category>
      <category>조건부 함수</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/126</guid>
      <comments>https://devsite.tistory.com/entry/MySQL%EC%97%90%EC%84%9C-Oracle-DECODE-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry126comment</comments>
      <pubDate>Tue, 20 May 2025 21:26:49 +0900</pubDate>
    </item>
    <item>
      <title>오라클 DECODE 함수 완벽 가이드: 데이터 변환의 마법 도구</title>
      <link>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-DECODE-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%ED%99%98%EC%9D%98-%EB%A7%88%EB%B2%95-%EB%8F%84%EA%B5%AC</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;oracle-logo-16x9_1920-1080.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SHmzJ/btsN4KvdEPZ/FYwNJkDb9LWOwhdwqqtn4K/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SHmzJ/btsN4KvdEPZ/FYwNJkDb9LWOwhdwqqtn4K/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SHmzJ/btsN4KvdEPZ/FYwNJkDb9LWOwhdwqqtn4K/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSHmzJ%2FbtsN4KvdEPZ%2FFYwNJkDb9LWOwhdwqqtn4K%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;274&quot; height=&quot;154&quot; data-filename=&quot;oracle-logo-16x9_1920-1080.webp&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 데이터베이스의 &lt;b&gt;DECODE 함수&lt;/b&gt;가 최근 데이터 전문가들 사이에서 다시 주목받고 있습니다. 이 강력한 함수는 복잡한 조건부 로직을 간단하게 구현할 수 있어 데이터 처리 효율성을 크게 향상시킵니다. SQL에서 조건문을 처리하는 방식을 혁신적으로 바꾼 DECODE는 다양한 산업 분야에서 데이터 처리 시간을 평균 15% 단축시킨다는 연구 결과도 있습니다. 복잡한 데이터 변환을 왜 DECODE로 처리하면 더 효율적일까요? 최신 데이터베이스 기술에서도 여전히 DECODE가 중요한 이유는 무엇일까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. DECODE 함수란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE는 오라클 데이터베이스에서 &lt;b&gt;조건부 로직을 구현하는 특별한 함수&lt;/b&gt;입니다. 이 함수는 기본적으로 프로그래밍 언어의 IF-THEN-ELSE 문과 유사한 기능을 SQL 내에서 수행합니다. DECODE는 오라클의 독자적인 함수로, 다른 데이터베이스 시스템에서는 일반적으로 CASE 문을 사용하는 것과 대비됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE 함수의 기본 구문은 다음과 같습니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DECODE(expression, search1, result1, search2, result2, ..., default)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작동 방식은 간단합니다. DECODE는 첫 번째 인자(expression)의 값을 평가한 후, 그 값이 search1과 일치하면 result1을 반환하고, search2와 일치하면 result2를 반환합니다. 어떤 search 값과도 일치하지 않으면 default 값(제공된 경우)을 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사례를 보면, 고객 등급 코드(1, 2, 3)를 사람이 읽기 쉬운 텍스트('프리미엄', '일반', '기본')로 변환하는 데 DECODE를 사용할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT customer_name, 
       DECODE(grade_code, 1, '프리미엄', 2, '일반', 3, '기본', '미분류') AS customer_grade
FROM customers;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. DECODE와 CASE 문의 차이점&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE 함수와 SQL 표준의 CASE 문은 &lt;b&gt;유사한 목적을 가지지만 중요한 차이점&lt;/b&gt;이 있습니다. DECODE는 오라클 전용 함수인 반면, CASE 문은 SQL 표준이며 대부분의 관계형 데이터베이스 관리 시스템(RDBMS)에서 지원됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 차이점은 다음과 같습니다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE는 &lt;b&gt;등식 비교만 가능&lt;/b&gt;한 반면, CASE 문은 다양한 비교 연산자(&amp;gt;, &amp;lt;, &amp;gt;=, &amp;lt;= 등)를 사용할 수 있습니다. 예를 들어, 점수에 따라 등급을 지정하는 경우 CASE 문이 더 적합합니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- CASE 문 사용
SELECT student_name,
       CASE 
           WHEN score &amp;gt;= 90 THEN 'A'
           WHEN score &amp;gt;= 80 THEN 'B'
           WHEN score &amp;gt;= 70 THEN 'C'
           ELSE 'D'
       END AS grade
FROM students;

-- DECODE로는 이런 범위 비교를 직접 구현할 수 없음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다중 조건 처리에서 &lt;b&gt;DECODE는 더 간결한 코드&lt;/b&gt;를 작성할 수 있다는 장점이 있습니다. 특히 중첩 DECODE를 사용하면 복잡한 로직도 효율적으로 표현할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. DECODE의 실용적 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE 함수는 다양한 실무 환경에서 유용하게 활용됩니다. 특히 &lt;b&gt;데이터 변환과 조건부 계산&lt;/b&gt;에서 그 가치가 빛납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 기관의 실제 사례를 살펴보면, A은행은 거래 코드를 사용자 친화적인 설명으로 변환하는 데 DECODE를 사용합니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT transaction_id, 
       amount,
       DECODE(trans_code, 
              'DEP', '입금', 
              'WTH', '출금', 
              'TRF', '계좌이체', 
              'INT', '이자', 
              '기타') AS transaction_type
FROM transactions;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 다른 사례로, 온라인 쇼핑몰은 지역별 배송비를 계산하는 데 DECODE를 활용합니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT order_id, 
       total_amount,
       DECODE(shipping_region, 
              'SEOUL', 3000, 
              'GYEONGGI', 5000, 
              'JEJU', 8000, 
              6000) AS shipping_fee
FROM orders;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의료 데이터 분석에서도 DECODE는 환자 상태 코드를 의미 있는 카테고리로 변환하는 데 사용됩니다. 이를 통해 의료진은 데이터를 더 쉽게 이해하고 분석할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. DECODE의 성능 최적화 기법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DECODE 함수를 효율적으로 사용하려면 &lt;b&gt;몇 가지 성능 최적화 기법&lt;/b&gt;을 고려해야 합니다. 대규모 데이터셋에서 DECODE를 사용할 때 특히 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;b&gt;인덱스된 열에 DECODE를 적용할 때 주의&lt;/b&gt;해야 합니다. DECODE 함수가 인덱스된 열에 적용되면 인덱스를 사용할 수 없게 만들 수 있어, 쿼리 성능이 저하될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 중첩 DECODE는 유용하지만 너무 깊은 중첩은 &lt;b&gt;가독성과 성능 모두에 좋지 않습니다&lt;/b&gt;. 3단계 이상의 중첩은 가능하면 피하고, 복잡한 로직은 여러 단계로 나누는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 성능 테스트에서, 대형 전자상거래 기업 B사는 상품 분류 시스템에서 복잡한 CASE 문을 DECODE로 대체한 후 쿼리 실행 시간이 약 20% 감소했다고 보고했습니다. 하지만 이는 특정 상황에 대한 결과이며, 항상 DECODE가 CASE보다 빠르다는 의미는 아닙니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 최신 오라클 버전에서의 DECODE 사용법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 데이터베이스의 최신 버전에서도 DECODE는 여전히 &lt;b&gt;강력한 기능을 제공&lt;/b&gt;하지만, 몇 가지 새로운 고려사항이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클 19c와 21c에서는 DECODE를 JSON 데이터 처리와 함께 사용하는 새로운 패턴이 등장했습니다. 예를 들어:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT j.customer_id,
       DECODE(JSON_VALUE(j.data, '$.status'), 
              'active', '활성', 
              'pending', '대기', 
              'suspended', '중지', 
              '상태 미정') AS account_status
FROM customers_json j;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 클라우드 기반 오라클 Autonomous Database에서는 &lt;b&gt;DECODE와 머신러닝 함수를 조합&lt;/b&gt;하는 고급 사용 사례도 나타나고 있습니다. 이러한 조합은 특히 예측 분석과 데이터 전처리 단계에서 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클의 최신 권장 사항에 따르면, 새로운 애플리케이션 개발 시에는 표준 준수와 이식성을 위해 CASE 문 사용을 권장하지만, DECODE는 레거시 시스템 유지보수와 특정 성능 최적화 시나리오에서 여전히 가치 있는 도구로 남아 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 실무자를 위한 DECODE 함수 활용 팁&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 개발자와 분석가를 위한 &lt;b&gt;실용적인 DECODE 함수 활용 팁&lt;/b&gt;을 소개합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 가독성을 높이기 위해 DECODE 문을 여러 줄로 포맷팅하는 것이 좋습니다. 특히 여러 조건이 있는 경우에는 더욱 중요합니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT product_name,
       DECODE(category_code,
              'ELEC', '전자제품',
              'FURN', '가구',
              'CLOTH', '의류',
              'BOOK', '도서',
              '기타') AS category_name
FROM products;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL 값 처리에 DECODE를 활용하는 것도 매우 유용합니다. DECODE는 NULL 값을 다른 값으로 쉽게 대체할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT employee_name,
       DECODE(department_id, NULL, '미배정', department_name) AS department
FROM employees, departments
WHERE employees.department_id = departments.department_id(+);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 금융 분석 회사 C사는 고객 데이터 보고서 생성 시 &lt;b&gt;DECODE를 활용해 코드 길이를 30% 줄이고&lt;/b&gt; 유지보수성을 향상시켰습니다. 이는 특히 복잡한 매핑 로직이 필요한 레거시 시스템 통합에서 큰 이점이었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리: DECODE의 미래와 대안 기술&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오라클의 DECODE 함수는 SQL에서 조건부 로직을 처리하는 강력하고 효율적인 도구입니다. 비록 오라클 전용 기능이지만, 그 간결함과 성능 이점으로 여전히 많은 개발자와 데이터 분석가들에게 사랑받고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 클라우드 네이티브 데이터베이스 환경과 멀티 데이터베이스 아키텍처가 확산됨에 따라, SQL 표준 준수의 중요성이 커질 것으로 예상됩니다. 그럼에도 불구하고 DECODE는 오라클 환경에서 데이터 변환과 조건부 로직 처리의 중요한 도구로 계속 자리매김할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>case 문</category>
      <category>decode</category>
      <category>SQL</category>
      <category>데이터 변환</category>
      <category>데이터 분석</category>
      <category>데이터베이스</category>
      <category>성능 최적화</category>
      <category>오라클</category>
      <category>오라클 19c</category>
      <category>조건부 로직</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/125</guid>
      <comments>https://devsite.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-DECODE-%ED%95%A8%EC%88%98-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%80%ED%99%98%EC%9D%98-%EB%A7%88%EB%B2%95-%EB%8F%84%EA%B5%AC#entry125comment</comments>
      <pubDate>Tue, 20 May 2025 21:23:53 +0900</pubDate>
    </item>
    <item>
      <title>STRAIGHT_JOIN 완전정복: 조인 순서를 제어하는 방법</title>
      <link>https://devsite.tistory.com/entry/STRAIGHTJOIN-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-%EC%A1%B0%EC%9D%B8-%EC%88%9C%EC%84%9C%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;container&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.jpg&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tujnm/btsNWMMhfWo/m1PBBxGKTyUj76nv70eaj1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tujnm/btsNWMMhfWo/m1PBBxGKTyUj76nv70eaj1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tujnm/btsNWMMhfWo/m1PBBxGKTyUj76nv70eaj1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftujnm%2FbtsNWMMhfWo%2Fm1PBBxGKTyUj76nv70eaj1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;179&quot; height=&quot;169&quot; data-filename=&quot;img.jpg&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;div class=&quot;section&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL로 복잡한 쿼리를 작성하다 보면 옵티마이저가 우리가 원하는 방식으로 실행 계획을 세우지 않을 때가 있습니다. 특히 여러 테이블을 조인할 때 최적이 아닌 순서를 선택해서 성능이 떨어지는 경우를 경험하신 적이 있으실 텐데요. 이때 &lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;이라는 강력한 도구를 사용하면 개발자가 직접 조인 순서를 제어할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 MySQL의 &lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;에 대해 자세히 알아보겠습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;STRAIGHT_JOIN이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;은 MySQL에서 제공하는 조인 힌트(Join Hint)의 일종으로, 쿼리 옵티마이저가 자동으로 결정하는 테이블 조인 순서를 무시하고 SQL 문에서 지정된 순서대로 테이블을 조인하도록 강제하는 기능입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 상황에서 MySQL 옵티마이저는 통계 정보와 비용 기반 모델을 사용해 최적의 조인 순서를 결정합니다. 하지만 때로는 옵티마이저가 잘못된 판단을 내릴 수 있고, 개발자가 더 나은 조인 순서를 알고 있는 경우가 있습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본 문법&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT STRAIGHT_JOIN column1, column2, ...
FROM table1, table2, table3, ...
WHERE condition;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 JOIN 구문과 함께 사용:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT column1, column2, ...
FROM table1 
STRAIGHT_JOIN table2 ON table1.id = table2.table1_id
STRAIGHT_JOIN table3 ON table2.id = table3.table2_id
WHERE condition;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;언제 사용해야 할까?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 옵티마이저가 비효율적인 선택을 할 때&lt;/h3&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;-- 일반적인 쿼리 (옵티마이저가 선택)
SELECT u.*, p.*, o.*
FROM users u
JOIN profiles p ON u.id = p.user_id
JOIN orders o ON u.id = o.user_id
WHERE u.active = 1;

-- STRAIGHT_JOIN으로 순서 강제
SELECT STRAIGHT_JOIN u.*, p.*, o.*  
FROM users u, profiles p, orders o
WHERE u.id = p.user_id 
  AND u.id = o.user_id
  AND u.active = 1;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 작은 테이블을 먼저 조인하고 싶을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;code&gt;users&lt;/code&gt; 테이블이 매우 작고, &lt;code&gt;orders&lt;/code&gt; 테이블이 매우 크다면, 작은 테이블을 먼저 필터링한 후 큰 테이블과 조인하는 것이 효율적일 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;ceylon&quot;&gt;&lt;code&gt;SELECT STRAIGHT_JOIN 
    small_table.*, 
    large_table.*
FROM small_lookup_table small_table,
     massive_fact_table large_table
WHERE small_table.id = large_table.lookup_id
  AND small_table.category = 'premium';&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 인덱스 활용을 최대화하고 싶을 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 순서로 조인했을 때 인덱스를 더 효율적으로 활용할 수 있는 경우:&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;SELECT STRAIGHT_JOIN 
    a.product_name,
    b.inventory_count
FROM products a,
     inventory b
WHERE a.product_id = b.product_id
  AND a.category_id = 123;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 성능 비교 예제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 실제 상황에서 &lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;의 효과를 보여주는 예제입니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 기본 쿼리 (2.3초 소요)
EXPLAIN SELECT 
    c.customer_name,
    o.order_date,
    oi.quantity
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
WHERE c.region = 'Asia'
  AND o.order_date &amp;gt;= '2024-01-01';

-- STRAIGHT_JOIN 사용 (0.8초 소요)
EXPLAIN SELECT STRAIGHT_JOIN
    c.customer_name,
    o.order_date,
    oi.quantity  
FROM customers c,
     orders o,
     order_items oi
WHERE c.customer_id = o.customer_id
  AND o.order_id = oi.order_id
  AND c.region = 'Asia'
  AND o.order_date &amp;gt;= '2024-01-01';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주의사항과 베스트 프랙티스&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 잘못 사용하면 성능 저하&lt;/h3&gt;
&lt;div class=&quot;warning&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;을 무작정 사용하면 오히려 성능이 저하될 수 있습니다:&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 나쁜 예: 큰 테이블을 먼저 읽고 작은 테이블과 조인
SELECT STRAIGHT_JOIN *
FROM huge_table h,
     small_table s
WHERE h.id = s.huge_id;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 실행 계획 분석은 필수&lt;/h3&gt;
&lt;div class=&quot;highlight&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;을 사용하기 전에 항상 &lt;code&gt;EXPLAIN&lt;/code&gt;으로 실행 계획을 분석하세요:&lt;/p&gt;
&lt;/div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 기본 쿼리 분석
EXPLAIN SELECT ... FROM table1 JOIN table2 ...;

-- STRAIGHT_JOIN 쿼리 분석
EXPLAIN SELECT STRAIGHT_JOIN ... FROM table1, table2 ...;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 데이터 변화에 민감&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 데이터가 변화함에 따라 최적의 조인 순서도 바뀔 수 있습니다. 정기적으로 쿼리 성능을 모니터링하고 필요시 조정해야 합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;다른 조인 힌트와 비교&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 제공하는 다른 힌트들과 비교해보면:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 인덱스 힌트
SELECT * FROM table1 USE INDEX (idx_name) WHERE ...;

-- 조인 방법 힌트 (MySQL 8.0+)
SELECT /*+ NESTED_LOOP(t1, t2) */ * FROM t1 JOIN t2 ...;

-- 조인 순서 힌트 (MySQL 8.0+)  
SELECT /*+ JOIN_ORDER(t1, t2, t3) */ * FROM t1 JOIN t2 JOIN t3 ...;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무에서의 활용 팁&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 점진적 적용&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 1단계: 실행 계획 분석
EXPLAIN SELECT ... FROM t1 JOIN t2 JOIN t3 ...;

-- 2단계: STRAIGHT_JOIN 테스트
EXPLAIN SELECT STRAIGHT_JOIN ... FROM t1, t2, t3 ...;

-- 3단계: 성능 측정
-- 실제 쿼리 실행 시간 비교&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 자동화된 점검&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 쿼리 성능 모니터링 쿼리
SELECT 
    query_text,
    execution_time,
    examined_rows
FROM performance_schema.events_statements_history
WHERE query_text LIKE '%STRAIGHT_JOIN%';&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;은 MySQL에서 조인 순서를 세밀하게 제어할 수 있는 강력한 도구입니다. 하지만 무작정 사용하기보다는 충분한 테스트와 분석을 거쳐 신중하게 적용해야 합니다.&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 포인트를 정리하면:&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;옵티마이저의 선택이 최적이 아닐 때 사용&lt;/li&gt;
&lt;li&gt;반드시 &lt;code&gt;EXPLAIN&lt;/code&gt;으로 실행 계획 확인&lt;/li&gt;
&lt;li&gt;정기적인 성능 모니터링 필요&lt;/li&gt;
&lt;li&gt;데이터 특성 변화에 따른 조정 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 &lt;code&gt;STRAIGHT_JOIN&lt;/code&gt;을 올바르게 활용하면 복잡한 쿼리의 성능을 크게 개선할 수 있습니다. 여러분의 프로젝트에서도 적절히 활용해보시기 바랍니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>DB/MySql</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/124</guid>
      <comments>https://devsite.tistory.com/entry/STRAIGHTJOIN-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-%EC%A1%B0%EC%9D%B8-%EC%88%9C%EC%84%9C%EB%A5%BC-%EC%A0%9C%EC%96%B4%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95#entry124comment</comments>
      <pubDate>Tue, 13 May 2025 21:23:55 +0900</pubDate>
    </item>
    <item>
      <title>SQL 쿼리 최적화: 키 컬럼과 조건절 순서의 영향</title>
      <link>https://devsite.tistory.com/entry/SQL-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%82%A4-%EC%BB%AC%EB%9F%BC%EA%B3%BC-%EC%A1%B0%EA%B1%B4%EC%A0%88-%EC%88%9C%EC%84%9C%EC%9D%98-%EC%98%81%ED%96%A5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;container&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 튜닝과 최적화에 대해 공부하다 보면 항상 궁금한 질문이 생깁니다. &quot;&lt;b&gt;테이블의 키 컬럼 순서와 WHERE 절의 조건 순서가 쿼리 실행 속도에 영향을 미칠까?&lt;/b&gt;&quot; 이 글에서는 이 질문에 대한 답을 찾고, 실제 업무에서 쿼리 성능을 향상시키는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;조건절 순서가 쿼리 실행 속도에 영향을 미치는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하자면, &lt;span class=&quot;highlight&quot;&gt;현대적인 RDBMS에서 WHERE 절의 조건 순서는 일반적으로 쿼리 성능에 직접적인 영향을 미치지 않습니다&lt;/span&gt;. 대부분의 데이터베이스 시스템은 강력한 쿼리 최적화기(Query Optimizer)를 갖추고 있어 개발자가 작성한 조건절 순서와 무관하게 가장 효율적인 실행 계획을 생성합니다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;b&gt;쿼리 최적화기란?&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리 최적화기는 SQL 쿼리를 분석하고 데이터베이스의 통계 정보, 인덱스 구조, 데이터 분포 등을 고려하여 가장 효율적인 방법으로 쿼리를 실행하기 위한 계획을 수립하는 데이터베이스 엔진의 핵심 구성 요소입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 몇 가지 중요한 예외와 고려사항이 있습니다. 이제 자세히 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인덱스 구성과 조건절 순서의 관계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 인덱스(Composite Index)를 사용할 때는 인덱스 컬럼의 순서가 쿼리 성능에 영향을 미칠 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복합 인덱스의 작동 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 (A, B, C) 순서로 생성된 복합 인덱스가 있다고 가정해 보겠습니다. 이 인덱스는 B-트리 구조로 A 컬럼을 기준으로 먼저 정렬된 후, A 값이 같은 레코드들은 B 컬럼으로 정렬되고, A와 B 값이 모두 같은 레코드들은 다시 C 컬럼으로 정렬됩니다.&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;b&gt;복합 인덱스 활용의 핵심&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복합 인덱스는 &quot;왼쪽 접두사 규칙(Left-prefix Rule)&quot;을 따릅니다. 즉, 인덱스의 첫 번째 컬럼(가장 왼쪽)부터 순서대로 사용해야 인덱스가 효율적으로 활용됩니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인덱스 활용 예시&lt;/h3&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 좋은 인덱스 활용 (인덱스 선두 컬럼 사용)
WHERE A = 1 AND B = 2

-- 좋은 인덱스 활용 (인덱스 전체 사용)
WHERE A = 1 AND B = 2 AND C = 3

-- 인덱스 효율 감소 (선두 컬럼 A 없음)
WHERE B = 2 AND C = 3

-- 인덱스 부분 활용 (A만 사용)
WHERE A = 1

-- 일반적으로 조건 순서는 중요하지 않음
WHERE C = 3 AND A = 1 AND B = 2  -- 최적화기가 재정렬함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WHERE 절에서 조건의 나열 순서는 대부분의 데이터베이스에서 최적화기가 적절히 재정렬하므로 크게 중요하지 않습니다. 그러나 &lt;b&gt;인덱스 설계 시 컬럼 순서&lt;/b&gt;는 매우 중요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스별 최적화기 특성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 데이터베이스 시스템마다 쿼리 최적화기의 동작 방식에 차이가 있습니다. 이러한 차이는 조건절 순서가 성능에 미치는 영향에도 차이를 가져올 수 있습니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;데이터베이스&lt;/th&gt;
&lt;th&gt;최적화기 특성&lt;/th&gt;
&lt;th&gt;조건절 순서의 영향&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;강력한 비용 기반 최적화기(CBO)를 사용&lt;/td&gt;
&lt;td&gt;일반적으로 조건절 순서에 크게 영향받지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;비용 기반 최적화기와 통계 정보를 활용&lt;/td&gt;
&lt;td&gt;조건절 순서보다 통계 정보의 정확성이 더 중요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;버전에 따라 다양한 최적화 기능 지원&lt;/td&gt;
&lt;td&gt;특히 오래된 버전에서는 조건절 순서가 영향을 미칠 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;정교한 최적화기와 다양한 실행 계획 옵션 제공&lt;/td&gt;
&lt;td&gt;대부분 조건절 순서와 무관하게 최적화&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL의 특별한 고려사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 경우, 특히 5.7 이전 버전에서는 조건절 순서가 실행 계획에 영향을 미치는 경우가 있습니다. 특히 인덱스 힌트를 사용하거나 특정 스토리지 엔진의 동작 방식에 따라 차이가 발생할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;MySQL에서 주의할 점&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 8.0 이상에서는 최적화기가 크게 개선되었지만, 여전히 복잡한 쿼리에서는 조건절 순서에 따른 실행 계획의 차이가 있을 수 있습니다. &lt;code&gt;EXPLAIN&lt;/code&gt;을 통해 실제 실행 계획을 확인하는 것이 중요합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단락 평가(Short-circuit Evaluation)의 영향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 데이터베이스는 논리 연산자 평가 시 단락 평가 방식을 사용합니다. 이는 조건절의 순서가 일부 상황에서 성능에 영향을 미칠 수 있음을 의미합니다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;-- 단락 평가의 예
WHERE condition1 AND condition2 AND condition3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 쿼리에서 condition1이 거짓으로 평가되면, 데이터베이스 엔진은 condition2와 condition3을 평가하지 않고 즉시 해당 행을 결과에서 제외합니다. 이런 특성을 활용하면 성능을 최적화할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;효율적인 조건절 배치 전략&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;선택도가 높은 조건을 먼저 배치&lt;/b&gt;: 결과 집합을 빠르게 줄일 수 있는 조건을 먼저 놓습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;계산 비용이 낮은 조건을 먼저 배치&lt;/b&gt;: 단순 비교가 함수 호출보다 먼저 오는 것이 유리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스를 활용하는 조건을 먼저 고려&lt;/b&gt;: 인덱싱된 컬럼에 대한 조건이 우선적으로 평가되도록 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;b&gt;선택도(Selectivity)란?&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건에 의해 필터링되는 행의 비율을 의미합니다. 선택도가 높다는 것은 많은 행을 필터링한다는 의미로, 100만 행 중 10행만 선택되는 조건은 선택도가 높은 조건입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;함수 호출이 포함된 조건의 영향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 호출이 포함된 조건은 평가 비용이 높을 수 있습니다. 이런 경우 단락 평가를 효과적으로 활용하면 성능을 향상시킬 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 더 효율적인 방식
WHERE simple_column = 1 AND expensive_function(column) = 'value'

-- 덜 효율적인 방식
WHERE expensive_function(column) = 'value' AND simple_column = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 예제에서는 simple_column 조건이 거짓이면 비용이 큰 함수 호출을 건너뛸 수 있습니다. 두 번째 예제에서는 함수가 항상 먼저 호출되어 불필요한 연산이 발생할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;b&gt;인덱싱 불가능한 함수 조건&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WHERE UPPER(last_name) = 'SMITH'&lt;/code&gt;와 같이 컬럼에 함수를 적용한 조건은 일반적으로 인덱스를 활용할 수 없습니다. 단, 함수 기반 인덱스(Function-based Index)를 생성했거나 대소문자 구분 없는 인덱스를 사용하는 경우는 예외입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 업무에서의 최적화 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론적인 내용을 바탕으로, 실제 업무에서 쿼리 성능을 향상시키기 위한 전략을 알아보겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 인덱스 설계 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건절 순서보다 인덱스 설계가 쿼리 성능에 더 큰 영향을 미칩니다. 효과적인 인덱스 설계 방법은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 자주 사용되는 조건 컬럼을 인덱스 선두에 배치&lt;/li&gt;
&lt;li&gt;높은 선택도(카디널리티)를 가진 컬럼을 인덱스 앞쪽에 배치&lt;/li&gt;
&lt;li&gt;범위 조건에 사용되는 컬럼은 인덱스 마지막에 배치&lt;/li&gt;
&lt;li&gt;자주 함께 사용되는 조건 컬럼들을 하나의 복합 인덱스로 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 실행 계획 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리의 실제 실행 방식을 이해하려면 실행 계획을 분석하는 것이 중요합니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Oracle
EXPLAIN PLAN FOR SELECT * FROM employees WHERE department_id = 10 AND salary &amp;gt; 5000;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- SQL Server
SET SHOWPLAN_ALL ON;
GO
SELECT * FROM employees WHERE department_id = 10 AND salary &amp;gt; 5000;
GO
SET SHOWPLAN_ALL OFF;

-- MySQL
EXPLAIN SELECT * FROM employees WHERE department_id = 10 AND salary &amp;gt; 5000;

-- PostgreSQL
EXPLAIN ANALYZE SELECT * FROM employees WHERE department_id = 10 AND salary &amp;gt; 5000;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 조건절 최적화 기법&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;범위 조건 최소화&lt;/b&gt;: 등호(=) 조건이 부등호 조건보다 인덱스를 효율적으로 활용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IN 대신 EXISTS 고려&lt;/b&gt;: 대량의 서브쿼리 결과를 처리할 때 EXISTS가 더 효율적일 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LIKE 패턴 최적화&lt;/b&gt;: 'abc%'는 인덱스를 활용할 수 있지만 '%abc'는 인덱스 스캔 불가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OR 대신 UNION ALL 고려&lt;/b&gt;: 복잡한 OR 조건은 때로 UNION ALL로 분리하는 것이 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;warning-box&quot;&gt;&lt;b&gt;NOT 연산자 주의&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WHERE NOT (condition)&lt;/code&gt; 또는 &lt;code&gt;WHERE column != value&lt;/code&gt;와 같은 부정 조건은 인덱스를 효율적으로 활용하기 어려울 수 있습니다. 가능하면 긍정 조건으로 변환하는 것이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스별 조건절 최적화 팁&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Oracle&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통계 정보를 최신 상태로 유지: &lt;code&gt;DBMS_STATS.GATHER_TABLE_STATS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;힌트(Hint)를 통한 실행 계획 제어: &lt;code&gt;/*+ INDEX(table_name index_name) */&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;결합 인덱스(Concatenated Index)와 함수 기반 인덱스(Function-Based Index) 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL Server&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리 저장소(Query Store)를 활용한 실행 계획 모니터링&lt;/li&gt;
&lt;li&gt;통계 정보 업데이트: &lt;code&gt;UPDATE STATISTICS table_name WITH FULLSCAN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;인덱싱된 계산 열(Computed Column)을 활용한 함수 조건 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 힌트 활용: &lt;code&gt;SELECT * FROM table_name FORCE INDEX (index_name) WHERE ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;스토리지 엔진에 따른 최적화 전략 차별화(InnoDB vs MyISAM)&lt;/li&gt;
&lt;li&gt;범위 조건에서 커버링 인덱스(Covering Index) 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부분 인덱스(Partial Index)를 활용한 조건절 최적화&lt;/li&gt;
&lt;li&gt;GIN, GiST 인덱스를 활용한 전문 검색 및 복잡한 데이터 타입 최적화&lt;/li&gt;
&lt;li&gt;병렬 쿼리 활용: &lt;code&gt;SET max_parallel_workers_per_gather = 4;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론: 조건절 순서와 쿼리 성능&lt;/h2&gt;
&lt;div class=&quot;conclusion&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블의 키 컬럼 순서와 조건절 순서가 쿼리 실행 속도에 미치는 영향을 살펴보았습니다. 핵심 내용을 정리하면:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;일반적으로 조건절 순서는 성능에 큰 영향을 미치지 않습니다.&lt;/b&gt; 현대 데이터베이스의 최적화기는 효율적인 실행 계획을 생성합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복합 인덱스 설계가 더 중요합니다.&lt;/b&gt; 인덱스 구성 컬럼 순서가 쿼리 성능에 직접적인 영향을 미칩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단락 평가와 함수 호출이 포함된 경우&lt;/b&gt; 조건절 순서가 성능에 영향을 미칠 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터베이스별 특성을 이해하고&lt;/b&gt; 그에 맞는 최적화 전략을 적용하는 것이 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실행 계획을 확인하고 분석하는 습관&lt;/b&gt;이 성능 최적화의 핵심입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;효율적인 데이터베이스 쿼리 작성을 위해서는 최적화기의 동작 원리를 이해하고, 적절한 인덱스 설계를 바탕으로 데이터베이스의 특성에 맞는 쿼리를 작성하는 것이 중요합니다. 조건절 순서보다는 전체적인 쿼리 구조와 인덱스 설계에 더 집중하는 것이 실질적인 성능 향상으로 이어질 것입니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;b&gt;성능 테스트의 중요성&lt;/b&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 환경과 데이터 분포에 따라 성능 양상이 달라질 수 있으므로, 중요한 쿼리의 경우 실제 환경에서 다양한 조건 배치와 인덱스 구성을 테스트하여 최적의 성능을 확인하는 것이 좋습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>DB</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/123</guid>
      <comments>https://devsite.tistory.com/entry/SQL-%EC%BF%BC%EB%A6%AC-%EC%B5%9C%EC%A0%81%ED%99%94-%ED%82%A4-%EC%BB%AC%EB%9F%BC%EA%B3%BC-%EC%A1%B0%EA%B1%B4%EC%A0%88-%EC%88%9C%EC%84%9C%EC%9D%98-%EC%98%81%ED%96%A5#entry123comment</comments>
      <pubDate>Sun, 11 May 2025 20:44:44 +0900</pubDate>
    </item>
    <item>
      <title>Oracle 소계쿼리의 모든 것: 데이터 분석의 강력한 도구</title>
      <link>https://devsite.tistory.com/entry/Oracle-Oracle-%EC%86%8C%EA%B3%84%EC%BF%BC%EB%A6%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%8F%84%EA%B5%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.jpg&quot; data-origin-width=&quot;2700&quot; data-origin-height=&quot;1414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFRyrs/btsNROLImAK/pMUBKjvnKKbPrLvU7bvCz1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFRyrs/btsNROLImAK/pMUBKjvnKKbPrLvU7bvCz1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFRyrs/btsNROLImAK/pMUBKjvnKKbPrLvU7bvCz1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFRyrs%2FbtsNROLImAK%2FpMUBKjvnKKbPrLvU7bvCz1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;384&quot; height=&quot;201&quot; data-filename=&quot;image.jpg&quot; data-origin-width=&quot;2700&quot; data-origin-height=&quot;1414&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 데이터베이스는 소계와 합계를 계산하기 위한 강력하고 다양한 기능을 제공합니다. Oracle은 데이터 분석을 위한 선두 주자로서, 풍부한 분석 함수와 집계 기능을 통해 복잡한 비즈니스 질문에 빠르게 답변할 수 있습니다. 이 문서에서는 Oracle에서 소계쿼리를 구현하는 다양한 방법과 고급 기능들을 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Oracle에서의 소계쿼리 개요&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서는 데이터의 다양한 레벨에서 소계와 합계를 계산하기 위해 여러 방법을 제공합니다. 주요 기능으로는 &lt;code&gt;ROLLUP&lt;/code&gt;, &lt;code&gt;CUBE&lt;/code&gt;, &lt;code&gt;GROUPING SETS&lt;/code&gt;이 있으며, 이들은 각각 다른 방식으로 다차원 데이터를 집계합니다.&lt;/p&gt;
&lt;div class=&quot;syntax&quot;&gt;&lt;b&gt;Oracle 소계쿼리 구문 요약:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;GROUP BY ROLLUP(col1, col2, ...)&lt;/code&gt;: 계층적 소계&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GROUP BY CUBE(col1, col2, ...)&lt;/code&gt;: 다차원 소계&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GROUP BY GROUPING SETS((col1, col2), (col1), ...)&lt;/code&gt;: 선택적 소계&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p class=&quot;version-note&quot; data-ke-size=&quot;size16&quot;&gt;참고: 이러한 기능들은 Oracle 9i부터 지원되었으며, Oracle 10g와 11g에서 확장되고 개선되었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ROLLUP을 사용한 계층적 소계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 &lt;code&gt;ROLLUP&lt;/code&gt;은 계층적인 순서로 소계를 생성합니다. &lt;code&gt;ROLLUP&lt;/code&gt;은 지정된 열에 대해 오른쪽에서 왼쪽으로 레벨을 감소시키며 집계를 수행합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Category, 
    SubCategory, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    ROLLUP(Region, Category, SubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 다음과 같은 집계 레벨을 생성합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Region, Category, SubCategory별 합계&lt;/li&gt;
&lt;li&gt;Region, Category별 합계 (SubCategory는 NULL)&lt;/li&gt;
&lt;li&gt;Region별 합계 (Category, SubCategory는 NULL)&lt;/li&gt;
&lt;li&gt;전체 합계 (모든 열이 NULL)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;부분 ROLLUP&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서는 부분 ROLLUP을 지원합니다. 이를 통해 특정 열에 대해서만 계층적 소계를 생성할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Year, 
    Region, 
    Category, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    Year, -- 이 열에 대한 소계는 생성하지 않음
    ROLLUP(Region, Category);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 Year에 대한 소계는 생성하지 않고, 각 Year 내에서 Region과 Category에 대한 소계만 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CUBE를 사용한 다차원 소계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 &lt;code&gt;CUBE&lt;/code&gt;는 지정된 모든 열의 가능한 조합에 대한 소계를 생성합니다. ROLLUP이 계층적 집계를 제공한다면, CUBE는 모든 차원 조합에 대한 분석을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Channel, 
    Category, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    CUBE(Region, Channel, Category);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 다음을 포함하는 총 8가지(2^3) 조합의 집계를 생성합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Region, Channel, Category별 합계&lt;/li&gt;
&lt;li&gt;Region, Channel별 합계 (Category는 NULL)&lt;/li&gt;
&lt;li&gt;Region, Category별 합계 (Channel은 NULL)&lt;/li&gt;
&lt;li&gt;Channel, Category별 합계 (Region은 NULL)&lt;/li&gt;
&lt;li&gt;Region별 합계 (Channel, Category는 NULL)&lt;/li&gt;
&lt;li&gt;Channel별 합계 (Region, Category는 NULL)&lt;/li&gt;
&lt;li&gt;Category별 합계 (Region, Channel은 NULL)&lt;/li&gt;
&lt;li&gt;전체 합계 (모든 열이 NULL)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부분 CUBE&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서는 부분 CUBE도 지원합니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Year, 
    Region, 
    Product, 
    Channel, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    Year, -- 이 열에 대한 소계는 생성하지 않음
    CUBE(Region, Product, Channel);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 Year에 대한 소계는 생성하지 않고, 각 Year 내에서 Region, Product, Channel의 모든 조합에 대한 소계를 생성합니다.&lt;/p&gt;
&lt;div class=&quot;tip&quot;&gt;&lt;b&gt;TIP:&lt;/b&gt; &lt;code&gt;CUBE&lt;/code&gt;는 2^n(n은 열의 수)개의 조합을 생성하므로, 열이 많을 경우 결과 집합이 매우 커질 수 있습니다. 이러한 경우 &lt;code&gt;GROUPING SETS&lt;/code&gt;를 사용하여 필요한 조합만 지정하는 것이 효율적입니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;GROUPING SETS을 사용한 선택적 소계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle의 &lt;code&gt;GROUPING SETS&lt;/code&gt;는 가장 유연한 집계 방법으로, 원하는 특정 그룹화 조합만 선택적으로 생성할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Product, 
    Channel, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    GROUPING SETS(
        (Region, Product, Channel), -- 가장 상세한 수준
        (Region, Product),          -- 지역 및 제품별
        (Channel),                  -- 채널별
        ()                          -- 전체 합계
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 지정된 4가지 그룹화 조합에 대한 결과만 생성합니다. CUBE와 달리 필요한 조합만 정확히 지정할 수 있어 더 효율적입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;GROUPING 관련 함수&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle은 소계 행을 식별하고 포맷하기 위한 여러 함수를 제공합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUPING 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GROUPING(column)&lt;/code&gt; 함수는 해당 열이 집계를 위해 NULL로 설정되었는지(값 1 반환) 아니면 실제 데이터의 NULL인지(값 0 반환) 식별합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE WHEN GROUPING(Region) = 1 THEN '전체 지역' ELSE Region END AS Region,
    CASE WHEN GROUPING(Category) = 1 THEN '전체 카테고리' ELSE Category END AS Category,
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    ROLLUP(Region, Category);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUPING_ID 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;GROUPING_ID(col1, col2, ...)&lt;/code&gt; 함수는 여러 열의 GROUPING 결과를 단일 정수 값으로 반환합니다. 이는 비트 벡터로 표현되어 어떤 열이 집계되었는지 식별하는 데 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Category, 
    SubCategory,
    GROUPING_ID(Region, Category, SubCategory) AS GID,
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    ROLLUP(Region, Category, SubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;tip&quot;&gt;&lt;b&gt;TIP:&lt;/b&gt; GROUPING_ID 값은 이진법으로 해석할 수 있습니다. 예를 들어, GID=3(이진수 011)은 SubCategory와 Category가 집계되었고 Region은 실제 값임을 의미합니다.&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUP_ID 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle은 &lt;code&gt;GROUP_ID()&lt;/code&gt; 함수도 제공하는데, 이는 중복 그룹을 식별하는 데 사용됩니다. 일반적으로 GROUPING SETS, ROLLUP 또는 CUBE를 함께 사용할 때 발생할 수 있는 중복 행을 제거하는 데 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Category, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    GROUPING SETS(
        (Region, Category),
        (Region, Category)  -- 중복된 그룹화 집합
    )
HAVING 
    GROUP_ID() = 0;  -- 첫 번째 중복 그룹만 유지&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Oracle에서의 복합 집계 예제&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서는 ROLLUP, CUBE, GROUPING SETS을 조합하여 복잡한 집계 시나리오를 구현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Year,
    Quarter,
    Region, 
    Category,
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    GROUPING SETS(
        ROLLUP(Year, Quarter),     -- 시간 차원의 계층적 집계
        CUBE(Region, Category)     -- 제품 및 지역 차원의 모든 조합
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 시간 차원에 대한 계층적 집계와 지역 및 제품 차원에 대한 다차원 집계를 결합합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실무 활용 예시: 재무 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 시간 계층별 부서 예산 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE WHEN GROUPING(Fiscal_Year) = 1 THEN '모든 연도' ELSE TO_CHAR(Fiscal_Year) END AS Year,
    CASE WHEN GROUPING(Quarter) = 1 THEN '전체 분기' ELSE Quarter END AS Quarter,
    CASE WHEN GROUPING(Department) = 1 THEN '전체 부서' ELSE Department END AS Department,
    SUM(Budget) AS TotalBudget,
    SUM(Actual) AS TotalSpent,
    ROUND(SUM(Actual) / NULLIF(SUM(Budget), 0) * 100, 2) AS UtilizationPct
FROM 
    FinancialData
WHERE 
    Fiscal_Year BETWEEN 2022 AND 2024
GROUP BY 
    ROLLUP(Fiscal_Year, Quarter, Department)
ORDER BY 
    GROUPING(Fiscal_Year), Fiscal_Year,
    GROUPING(Quarter), Quarter,
    GROUPING(Department), Department;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 지역 및 제품별 판매 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    DECODE(GROUPING(Region), 1, '전체 지역', Region) AS Region,
    DECODE(GROUPING(Channel), 1, '전체 채널', Channel) AS Channel,
    DECODE(GROUPING(Category), 1, '전체 카테고리', Category) AS Category,
    SUM(Sales) AS TotalSales,
    COUNT(DISTINCT Customer_ID) AS CustomerCount,
    ROUND(SUM(Sales) / COUNT(DISTINCT Customer_ID), 2) AS AvgSalePerCustomer,
    RANK() OVER (
        PARTITION BY 
            GROUPING(Region), 
            GROUPING(Channel), 
            GROUPING(Category) 
        ORDER BY 
            SUM(Sales) DESC
    ) AS SalesRank
FROM 
    SalesData
WHERE 
    Sale_Date BETWEEN TO_DATE('2024-01-01', 'YYYY-MM-DD') AND TO_DATE('2024-03-31', 'YYYY-MM-DD')
GROUP BY 
    CUBE(Region, Channel, Category)
ORDER BY 
    GROUPING(Region), Region,
    GROUPING(Channel), Channel,
    GROUPING(Category), Category;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 선택적 그룹화를 통한 매출 트렌드 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE 
        WHEN GROUPING(Year) = 1 AND GROUPING(Month) = 1 THEN '전체 기간'
        WHEN GROUPING(Month) = 1 THEN Year || ' 전체'
        ELSE Year || '-' || LPAD(Month, 2, '0')
    END AS TimePeriod,
    CASE WHEN GROUPING(Product_Line) = 1 THEN '전체 제품라인' ELSE Product_Line END AS ProductLine,
    SUM(Sales) AS TotalSales,
    SUM(Sales) - LAG(SUM(Sales)) OVER (
        PARTITION BY GROUPING(Product_Line), Product_Line 
        ORDER BY Year, Month NULLS LAST
    ) AS SalesGrowth,
    ROUND((SUM(Sales) / LAG(SUM(Sales)) OVER (
        PARTITION BY GROUPING(Product_Line), Product_Line 
        ORDER BY Year, Month NULLS LAST
    ) - 1) * 100, 2) AS GrowthPct
FROM 
    SalesData
WHERE 
    Year BETWEEN 2022 AND 2024
GROUP BY 
    GROUPING SETS(
        (Year, Month, Product_Line),  -- 월별, 제품라인별
        (Year, Product_Line),         -- 연도별, 제품라인별
        (Product_Line),               -- 제품라인별 전체
        ()                            -- 전체 합계
    )
ORDER BY 
    Year NULLS LAST, Month NULLS LAST, 
    GROUPING(Product_Line), Product_Line;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Oracle의 PIVOT 및 UNPIVOT&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 11g부터 도입된 &lt;code&gt;PIVOT&lt;/code&gt;과 &lt;code&gt;UNPIVOT&lt;/code&gt; 연산자를 사용하면 행과 열을 전환하는 작업을 쉽게 수행할 수 있으며, 이를 소계 쿼리와 결합할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 소계가 포함된 피벗 테이블 생성
WITH RollupData AS (
    SELECT 
        CASE WHEN GROUPING(Category) = 1 THEN '총계' ELSE Category END AS Category,
        Quarter,
        Sales
    FROM 
        SalesData
    WHERE 
        Year = 2024
    GROUP BY 
        ROLLUP(Category), Quarter
)
SELECT * FROM RollupData
PIVOT (
    SUM(Sales)
    FOR Quarter IN ('Q1' AS Q1, 'Q2' AS Q2, 'Q3' AS Q3, 'Q4' AS Q4)
)
ORDER BY 
    CASE WHEN Category = '총계' THEN 1 ELSE 0 END, Category;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 각 카테고리와 총계에 대해 분기별 매출을 열로 표시하는 피벗 테이블을 생성합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Q1&lt;/th&gt;
&lt;th&gt;Q2&lt;/th&gt;
&lt;th&gt;Q3&lt;/th&gt;
&lt;th&gt;Q4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Electronics&lt;/td&gt;
&lt;td&gt;125000&lt;/td&gt;
&lt;td&gt;138000&lt;/td&gt;
&lt;td&gt;142000&lt;/td&gt;
&lt;td&gt;185000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Furniture&lt;/td&gt;
&lt;td&gt;84000&lt;/td&gt;
&lt;td&gt;92000&lt;/td&gt;
&lt;td&gt;87000&lt;/td&gt;
&lt;td&gt;110000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clothing&lt;/td&gt;
&lt;td&gt;67000&lt;/td&gt;
&lt;td&gt;75000&lt;/td&gt;
&lt;td&gt;82000&lt;/td&gt;
&lt;td&gt;115000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총계&lt;/td&gt;
&lt;td&gt;276000&lt;/td&gt;
&lt;td&gt;305000&lt;/td&gt;
&lt;td&gt;311000&lt;/td&gt;
&lt;td&gt;410000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;분석 함수와 소계 결합하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle은 풍부한 분석 함수(윈도우 함수)를 제공하며, 이를 소계 쿼리와 결합하여 고급 분석을 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;WITH SalesRollup AS (
    SELECT 
        CASE WHEN GROUPING(Region) = 1 THEN '전체 지역' ELSE Region END AS Region,
        CASE WHEN GROUPING(Category) = 1 THEN '전체 카테고리' ELSE Category END AS Category,
        SUM(Sales) AS TotalSales,
        GROUPING_ID(Region, Category) AS GID
    FROM 
        SalesData
    WHERE 
        Year = 2024
    GROUP BY 
        ROLLUP(Region, Category)
)
SELECT 
    Region,
    Category,
    TotalSales,
    -- 전체 대비 비율
    CASE 
        WHEN GID &amp;lt; 3 THEN 
            ROUND(TotalSales / SUM(CASE WHEN GID = 3 THEN TotalSales END) OVER() * 100, 2)
        ELSE NULL
    END AS PctOfTotal,
    -- 지역 내 비율 (카테고리별)
    CASE 
        WHEN GID = 0 THEN 
            ROUND(TotalSales / SUM(CASE WHEN GID = 1 AND Region = Region THEN TotalSales END) OVER(PARTITION BY Region) * 100, 2)
        ELSE NULL
    END AS PctInRegion,
    -- 누적 합계 (지역 내)
    CASE 
        WHEN GID = 0 THEN 
            SUM(TotalSales) OVER(PARTITION BY Region ORDER BY TotalSales DESC)
        ELSE NULL
    END AS RunningTotalInRegion
FROM 
    SalesRollup
ORDER BY 
    GROUPING(Region), Region,
    GROUPING(Category), TotalSales DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 소계 행과 함께 전체 대비 비율, 지역 내 카테고리별 비율, 지역 내 누적 합계와 같은 고급 분석 지표를 계산합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;성능 최적화 기법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle에서 소계쿼리의 성능을 최적화하기 위한 몇 가지 중요한 기법은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;물리적 설계 최적화:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GROUP BY 열에 인덱스 생성&lt;/li&gt;
&lt;li&gt;파티셔닝을 활용하여 데이터 액세스 최소화&lt;/li&gt;
&lt;li&gt;집계 열에 대한 함수 기반 인덱스 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통계 분석 활용:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정기적으로 &lt;code&gt;DBMS_STATS.GATHER_TABLE_STATS&lt;/code&gt; 실행&lt;/li&gt;
&lt;li&gt;히스토그램을 활용하여 데이터 분포에 따른 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 튜닝:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 열만 선택하여 처리할 데이터 양 최소화&lt;/li&gt;
&lt;li&gt;WHERE 절에서 가능한 한 많은 데이터 필터링&lt;/li&gt;
&lt;li&gt;데이터 볼륨이 큰 경우 GROUPING SETS 사용 고려(CUBE 대신)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구체화된 뷰(Materialized Views) 활용:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자주 사용되는 집계 쿼리에 대해 구체화된 뷰 생성&lt;/li&gt;
&lt;li&gt;자동 쿼리 재작성 활성화: &lt;code&gt;ALTER SESSION SET QUERY_REWRITE_ENABLED = TRUE;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;증분 리프레시 설정하여 데이터 최신성 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;warn&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; Oracle의 ROLLUP, CUBE, GROUPING SETS은 임시 테이블 작업을 필요로 하므로, 대용량 데이터에서는 TEMP 테이블스페이스 크기와 PGA/SGA 메모리 설정에 주의해야 합니다. 특히 CUBE는 2^n 조합을 생성하므로 열 수가 많을 경우 성능 영향이 클 수 있습니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Oracle, MSSQL, MySQL 소계쿼리 비교&lt;/b&gt;&lt;/h2&gt;
&lt;div class=&quot;comparison&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;th&gt;MSSQL&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기본 소계 구문&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY ROLLUP(...)&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;Oracle 9i+&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY ROLLUP(...)&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;SQL Server 2008+&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY ... WITH ROLLUP&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;MySQL 5.0+&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다차원 소계&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY CUBE(...)&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;Oracle 9i+&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY CUBE(...)&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;SQL Server 2008+&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;직접 지원하지 않음&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;(UNION ALL로 구현 가능)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택적 그룹화&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY GROUPING SETS(...)&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;Oracle 9i+&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY GROUPING SETS(...)&lt;/code&gt;&lt;br /&gt;&lt;span class=&quot;version-note&quot;&gt;SQL Server 2008+&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</description>
      <category>DB/Oracle</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/122</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-Oracle-%EC%86%8C%EA%B3%84%EC%BF%BC%EB%A6%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%8F%84%EA%B5%AC#entry122comment</comments>
      <pubDate>Sun, 11 May 2025 20:28:29 +0900</pubDate>
    </item>
    <item>
      <title>MSSQL 소계쿼리의 모든 것: 데이터 분석의 강력한 도구</title>
      <link>https://devsite.tistory.com/entry/MSSQL-%EC%86%8C%EA%B3%84%EC%BF%BC%EB%A6%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%8F%84%EA%B5%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;microsoft-sql-server-logo.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UfNz0/btsNTHxgOhC/EhwwdHKMrjCjYEJkOtDRZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UfNz0/btsNTHxgOhC/EhwwdHKMrjCjYEJkOtDRZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UfNz0/btsNTHxgOhC/EhwwdHKMrjCjYEJkOtDRZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUfNz0%2FbtsNTHxgOhC%2FEhwwdHKMrjCjYEJkOtDRZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;219&quot; height=&quot;119&quot; data-filename=&quot;microsoft-sql-server-logo.png&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Microsoft SQL Server(MSSQL)에서 소계와 합계를 계산하는 것은 데이터 분석과 보고서 생성에 필수적인 기능입니다. 이 문서에서는 MSSQL에서 소계쿼리를 구현하는 다양한 방법과 고급 기능들을 살펴보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MSSQL에서의 소계쿼리란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소계쿼리는 데이터를 서로 다른 수준에서 집계하여 세부 데이터와 함께 중간 합계 및 총계를 한번에 제공하는 쿼리입니다. MSSQL은 이를 위한 여러 기능을 제공하며, 가장 대표적인 것이 &lt;code&gt;ROLLUP&lt;/code&gt;, &lt;code&gt;CUBE&lt;/code&gt;, &lt;code&gt;GROUPING SETS&lt;/code&gt;입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;ROLLUP을 사용한 소계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL에서 &lt;code&gt;ROLLUP&lt;/code&gt;은 계층적 소계를 생성하는 데 사용됩니다. MySQL의 &lt;code&gt;WITH ROLLUP&lt;/code&gt;과 유사하지만 구문에 차이가 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Category, 
    SubCategory, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    Category, SubCategory
    WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL 2008 이상에서는 다음과 같이 &lt;code&gt;GROUP BY&lt;/code&gt;와 &lt;code&gt;ROLLUP&lt;/code&gt;을 함께 사용합니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Category, 
    SubCategory, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    ROLLUP(Category, SubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 다음과 같은 결과를 생성합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Category와 SubCategory 조합에 대한 합계&lt;/li&gt;
&lt;li&gt;각 Category의 합계 (SubCategory는 NULL)&lt;/li&gt;
&lt;li&gt;전체 합계 (Category와 SubCategory 모두 NULL)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;GROUPING 함수&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL에서는 &lt;code&gt;GROUPING&lt;/code&gt; 함수를 사용하여 NULL 값이 실제 데이터의 NULL인지 아니면 소계를 위한 NULL인지 구분할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE 
        WHEN GROUPING(Category) = 1 THEN '모든 카테고리' 
        ELSE Category 
    END AS Category,
    CASE 
        WHEN GROUPING(SubCategory) = 1 THEN '모든 서브카테고리' 
        ELSE SubCategory 
    END AS SubCategory,
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    ROLLUP(Category, SubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 MSSQL은 &lt;code&gt;GROUPING_ID&lt;/code&gt; 함수를 제공하여 여러 열의 GROUPING 결과를 단일 정수 값으로 표현할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Category, 
    SubCategory, 
    GROUPING_ID(Category, SubCategory) AS GroupingLevel,
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    ROLLUP(Category, SubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;부분 ROLLUP&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL에서는 부분 ROLLUP을 구현할 수 있습니다. 이는 일부 열에 대해서만 소계를 생성하고 싶을 때 유용합니다:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Category, 
    SubCategory, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    Region, -- 이 열에 대한 소계는 생성하지 않음
    ROLLUP(Category, SubCategory);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 Region에 대한 소계는 생성하지 않고, Category와 SubCategory에 대한 소계만 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;CUBE를 사용한 다차원 소계&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL의 &lt;code&gt;CUBE&lt;/code&gt;는 &lt;code&gt;ROLLUP&lt;/code&gt;보다 더 많은 조합의 소계를 생성합니다. &lt;code&gt;CUBE&lt;/code&gt;는 지정된 모든 열의 가능한 조합에 대한 소계를 생성합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Category, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    CUBE(Region, Category);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 다음과 같은 결과를 생성합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 Region과 Category 조합에 대한 합계&lt;/li&gt;
&lt;li&gt;각 Region의 합계 (Category는 NULL)&lt;/li&gt;
&lt;li&gt;각 Category의 합계 (Region은 NULL)&lt;/li&gt;
&lt;li&gt;전체 합계 (Region과 Category 모두 NULL)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;tip&quot;&gt;&lt;b&gt;TIP:&lt;/b&gt; &lt;code&gt;ROLLUP&lt;/code&gt;은 계층적 관계를 가진 데이터(예: 연도 &amp;gt; 분기 &amp;gt; 월)에 적합하고, &lt;code&gt;CUBE&lt;/code&gt;는 서로 독립적인 차원에 대한 분석(예: 지역과 제품 카테고리)에 적합합니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;GROUPING SETS&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL의 &lt;code&gt;GROUPING SETS&lt;/code&gt;를 사용하면 원하는 특정 그룹화 조합만 선택적으로 생성할 수 있습니다. 이는 &lt;code&gt;ROLLUP&lt;/code&gt;이나 &lt;code&gt;CUBE&lt;/code&gt;보다 더 세밀한 제어가 가능합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    Region, 
    Category, 
    Year, 
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    GROUPING SETS(
        (Region, Category, Year), -- 가장 상세한 수준
        (Region, Category),       -- Region과 Category별 합계
        (Year),                   -- Year별 합계
        ()                        -- 전체 합계
    );&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 지정된 4가지 그룹화 조합에 대한 결과만 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MSSQL 소계쿼리의 실무 활용 예시&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 다년간 지역별 판매 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE WHEN GROUPING(Year) = 1 THEN '전체 연도' ELSE CAST(Year AS VARCHAR) END AS Year,
    CASE WHEN GROUPING(Region) = 1 THEN '전체 지역' ELSE Region END AS Region,
    CASE WHEN GROUPING(Quarter) = 1 THEN '전체 분기' ELSE Quarter END AS Quarter,
    SUM(Sales) AS TotalSales,
    COUNT(DISTINCT CustomerID) AS CustomerCount,
    AVG(SalesAmount) AS AvgSalesAmount
FROM 
    SalesData
WHERE 
    Year BETWEEN 2022 AND 2024
GROUP BY 
    ROLLUP(Year, Region, Quarter)
ORDER BY 
    GROUPING(Year), Year,
    GROUPING(Region), Region,
    GROUPING(Quarter), Quarter;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 제품 카테고리 및 고객 유형별 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE WHEN GROUPING(Category) = 1 THEN '전체 카테고리' ELSE Category END AS Category,
    CASE WHEN GROUPING(CustomerType) = 1 THEN '전체 고객유형' ELSE CustomerType END AS CustomerType,
    SUM(Sales) AS TotalSales,
    COUNT(*) AS TransactionCount,
    SUM(Sales) / COUNT(*) AS AvgTransactionValue
FROM 
    SalesData
GROUP BY 
    CUBE(Category, CustomerType)
ORDER BY 
    GROUPING(Category), Category,
    GROUPING(CustomerType), CustomerType;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 맞춤형 그룹화를 통한 판매 채널 분석&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    CASE WHEN GROUPING(Year) = 1 THEN '전체 연도' ELSE CAST(Year AS VARCHAR) END AS Year,
    CASE WHEN GROUPING(Channel) = 1 THEN '전체 채널' ELSE Channel END AS Channel,
    CASE WHEN GROUPING(Region) = 1 THEN '전체 지역' ELSE Region END AS Region,
    SUM(Sales) AS TotalSales
FROM 
    SalesData
GROUP BY 
    GROUPING SETS(
        (Year, Channel, Region), -- 상세 수준
        (Year, Channel),         -- 연도 및 채널별
        (Channel, Region),       -- 채널 및 지역별
        (Channel),               -- 채널별
        ()                       -- 전체 합계
    )
ORDER BY 
    GROUPING(Year), Year,
    GROUPING(Channel), Channel,
    GROUPING(Region), Region;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;피벗 테이블과 소계쿼리 결합하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL에서는 &lt;code&gt;PIVOT&lt;/code&gt; 연산자를 사용하여 행을 열로 변환할 수 있으며, 이를 소계쿼리와 결합할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 임시 테이블로 소계 결과 저장
WITH SalesRollup AS (
    SELECT 
        CASE WHEN GROUPING(Category) = 1 THEN '총계' ELSE Category END AS Category,
        Quarter,
        Sales
    FROM 
        SalesData
    WHERE 
        Year = 2024
    GROUP BY 
        ROLLUP(Category), Quarter
)
-- PIVOT을 사용하여 분기별 매출을 열로 변환
SELECT 
    Category,
    [Q1] AS Q1_Sales,
    [Q2] AS Q2_Sales,
    [Q3] AS Q3_Sales,
    [Q4] AS Q4_Sales,
    ISNULL([Q1], 0) + ISNULL([Q2], 0) + ISNULL([Q3], 0) + ISNULL([Q4], 0) AS YearlyTotal
FROM 
    SalesRollup
PIVOT (
    SUM(Sales)
    FOR Quarter IN ([Q1], [Q2], [Q3], [Q4])
) AS PivotTable
ORDER BY 
    CASE WHEN Category = '총계' THEN 1 ELSE 0 END, Category;&lt;/code&gt;&lt;/pre&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Q1_Sales&lt;/th&gt;
&lt;th&gt;Q2_Sales&lt;/th&gt;
&lt;th&gt;Q3_Sales&lt;/th&gt;
&lt;th&gt;Q4_Sales&lt;/th&gt;
&lt;th&gt;YearlyTotal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;전자기기&lt;/td&gt;
&lt;td&gt;125000&lt;/td&gt;
&lt;td&gt;138000&lt;/td&gt;
&lt;td&gt;142000&lt;/td&gt;
&lt;td&gt;185000&lt;/td&gt;
&lt;td&gt;590000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가구&lt;/td&gt;
&lt;td&gt;84000&lt;/td&gt;
&lt;td&gt;92000&lt;/td&gt;
&lt;td&gt;87000&lt;/td&gt;
&lt;td&gt;110000&lt;/td&gt;
&lt;td&gt;373000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;67000&lt;/td&gt;
&lt;td&gt;75000&lt;/td&gt;
&lt;td&gt;82000&lt;/td&gt;
&lt;td&gt;115000&lt;/td&gt;
&lt;td&gt;339000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총계&lt;/td&gt;
&lt;td&gt;276000&lt;/td&gt;
&lt;td&gt;305000&lt;/td&gt;
&lt;td&gt;311000&lt;/td&gt;
&lt;td&gt;410000&lt;/td&gt;
&lt;td&gt;1302000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;순위 및 윈도우 함수와 소계 결합하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL의 윈도우 함수를 사용하면 소계와 함께 순위나 누적 합계 등의 고급 분석을 수행할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;WITH SalesRollup AS (
    SELECT 
        CASE WHEN GROUPING(Category) = 1 THEN '총계' ELSE Category END AS Category,
        CASE WHEN GROUPING(SubCategory) = 1 THEN 
            CASE WHEN GROUPING(Category) = 1 THEN '총계' ELSE '소계' END 
            ELSE SubCategory END AS SubCategory,
        SUM(Sales) AS TotalSales,
        GROUPING_ID(Category, SubCategory) AS Level
    FROM 
        SalesData
    GROUP BY 
        ROLLUP(Category, SubCategory)
)
SELECT 
    Category,
    SubCategory,
    TotalSales,
    CASE 
        WHEN Level = 0 THEN -- 가장 상세한 수준 (Category, SubCategory)
            RANK() OVER(PARTITION BY Category ORDER BY TotalSales DESC)
        ELSE NULL
    END AS RankInCategory,
    CASE 
        WHEN Level = 0 THEN -- 가장 상세한 수준 (Category, SubCategory)
            FORMAT(TotalSales / SUM(TotalSales) OVER(PARTITION BY Category, GROUPING(SubCategory)), 'P2')
        WHEN Level = 1 THEN -- Category 소계
            FORMAT(TotalSales / SUM(TotalSales) OVER(PARTITION BY GROUPING(Category)), 'P2')
        ELSE NULL
    END AS ContributionPct
FROM 
    SalesRollup
ORDER BY 
    Level, Category, TotalSales DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 각 카테고리 내에서 서브카테고리별 판매 순위와 기여도를 계산합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MSSQL 소계쿼리 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL에서 소계쿼리의 성능을 최적화하기 위한 몇 가지 팁입니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;필터링은 소계 계산 전에 수행하여 처리해야 할 데이터의 양을 줄입니다.&lt;/li&gt;
&lt;li&gt;필요한 열만 포함하여 메모리 사용량을 최소화합니다.&lt;/li&gt;
&lt;li&gt;GROUP BY 열에 인덱스를 생성하면 그룹화 작업 성능이 향상됩니다.&lt;/li&gt;
&lt;li&gt;매우 큰 데이터셋의 경우 다음과 같은 접근 방식을 고려하세요:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사전 계산된 집계 테이블 사용&lt;/li&gt;
&lt;li&gt;파티셔닝된 테이블 활용&lt;/li&gt;
&lt;li&gt;복잡한 쿼리를 여러 개의 작은 쿼리로 분할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPTION (RECOMPILE)&lt;/code&gt; 힌트를 사용하여 쿼리 계획을 최적화할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;warn&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; &lt;code&gt;CUBE&lt;/code&gt;는 &lt;code&gt;ROLLUP&lt;/code&gt;보다 더 많은 조합을 생성하므로 데이터가 많은 경우 리소스 사용량이 크게 증가할 수 있습니다. 필요한 경우에만 CUBE를 사용하고, 가능하면 &lt;code&gt;GROUPING SETS&lt;/code&gt;로 필요한 조합만 정의하는 것이 좋습니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;MySQL과 MSSQL 소계쿼리 비교&lt;/b&gt;&lt;/h2&gt;
&lt;div class=&quot;comparison&quot;&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;MSSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;기본 소계 구문&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY ... WITH ROLLUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY ROLLUP(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다차원 소계&lt;/td&gt;
&lt;td&gt;직접 지원하지 않음 (UNION ALL로 구현 가능)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY CUBE(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;선택적 그룹화&lt;/td&gt;
&lt;td&gt;직접 지원하지 않음 (UNION ALL로 구현 가능)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUP BY GROUPING SETS(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL 구분&lt;/td&gt;
&lt;td&gt;MySQL 8.0+: &lt;code&gt;GROUPING()&lt;/code&gt; 함수&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GROUPING()&lt;/code&gt; 및 &lt;code&gt;GROUPING_ID()&lt;/code&gt; 함수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부분 소계&lt;/td&gt;
&lt;td&gt;지원하지 않음&lt;/td&gt;
&lt;td&gt;지원 (&lt;code&gt;GROUP BY col1, ROLLUP(col2, col3)&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;피벗 테이블&lt;/td&gt;
&lt;td&gt;CASE 표현식으로 구현 (내장 함수 없음)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PIVOT&lt;/code&gt; 연산자 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSSQL은 MySQL보다 더 다양하고 강력한 소계 및 집계 기능을 제공합니다. &lt;code&gt;ROLLUP&lt;/code&gt;, &lt;code&gt;CUBE&lt;/code&gt;, &lt;code&gt;GROUPING SETS&lt;/code&gt; 및 &lt;code&gt;PIVOT&lt;/code&gt;과 같은 기능을 활용하면 복잡한 비즈니스 보고서를 효율적으로 생성할 수 있습니다. 각 기능의 특성과 성능 영향을 이해하고 적절하게 활용하면 데이터 분석 작업을 크게 향상시킬 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;tip&quot;&gt;&lt;b&gt;추가 학습 자료:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql&quot;&gt;Microsoft 공식 문서: GROUP BY (Transact-SQL)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/sql/t-sql/functions/grouping-transact-sql&quot;&gt;Microsoft 공식 문서: GROUPING 함수&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/sql/t-sql/queries/from-using-pivot-and-unpivot&quot;&gt;Microsoft 공식 문서: PIVOT 및 UNPIVOT&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>DB/MSSQL</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/121</guid>
      <comments>https://devsite.tistory.com/entry/MSSQL-%EC%86%8C%EA%B3%84%EC%BF%BC%EB%A6%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%8F%84%EA%B5%AC#entry121comment</comments>
      <pubDate>Sun, 11 May 2025 20:23:53 +0900</pubDate>
    </item>
    <item>
      <title>[MySQL] MySQL 소계쿼리의 모든 것: 데이터 분석의 강력한 도구</title>
      <link>https://devsite.tistory.com/entry/MySQL-%EC%86%8C%EA%B3%84%EC%BF%BC%EB%A6%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%8F%84%EA%B5%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.png&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RmJ12/btsNTzlVVTI/xxxBFYuckajj1SAvtKqfK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RmJ12/btsNTzlVVTI/xxxBFYuckajj1SAvtKqfK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RmJ12/btsNTzlVVTI/xxxBFYuckajj1SAvtKqfK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRmJ12%2FbtsNTzlVVTI%2FxxxBFYuckajj1SAvtKqfK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;384&quot; height=&quot;260&quot; data-filename=&quot;img1.daumcdn.png&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 분석에서 합계나 소계를 계산하는 것은 매우 중요한 작업입니다. MySQL에서는 이러한 작업을 수행하기 위한 다양한 방법을 제공하는데, 그 중에서도 소계쿼리(Subtotal Query)는 데이터를 다양한 레벨에서 집계할 수 있는 강력한 도구입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;소계쿼리란 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소계쿼리는 데이터를 그룹화하고 각 그룹별로 집계 값을 계산한 다음, 이러한 집계를 다양한 레벨에서 제공하는 쿼리입니다. 예를 들어, 제품별 판매량, 카테고리별 판매량, 그리고 전체 판매량을 한 번의 쿼리로 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GROUP BY와 WITH ROLLUP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서 소계를 구현하는 가장 기본적인 방법은 &lt;code&gt;GROUP BY&lt;/code&gt;절과 함께 &lt;code&gt;WITH ROLLUP&lt;/code&gt; 수정자를 사용하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT 
    category, 
    product, 
    SUM(sales) AS total_sales
FROM 
    sales_data
GROUP BY 
    category, product
WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 카테고리와 제품별로 판매량을 집계한 후, 추가적으로 다음과 같은 집계 행을 생성합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 카테고리의 총 판매량 (product 열이 NULL)&lt;/li&gt;
&lt;li&gt;전체 판매량 (category와 product 열이 모두 NULL)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GROUPING() 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WITH ROLLUP&lt;/code&gt;으로 생성된 소계 행에서는 NULL 값이 나타납니다. 이것이 실제 데이터의 NULL 값인지 소계를 위한 NULL 값인지 구분하기 위해 &lt;code&gt;GROUPING()&lt;/code&gt; 함수를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    IF(GROUPING(category) = 1, '모든 카테고리', category) AS category,
    IF(GROUPING(product) = 1, '모든 제품', product) AS product,
    SUM(sales) AS total_sales
FROM 
    sales_data
GROUP BY 
    category, product
WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 소계 행에 '모든 카테고리', '모든 제품'과 같은 레이블을 표시하여 가독성을 높입니다.&lt;/p&gt;
&lt;div class=&quot;tip&quot;&gt;&lt;b&gt;TIP:&lt;/b&gt; &lt;code&gt;GROUPING()&lt;/code&gt; 함수는 MySQL 8.0 이상에서 지원됩니다. 이전 버전에서는 &lt;code&gt;IFNULL()&lt;/code&gt;을 사용하여 유사한 결과를 얻을 수 있지만, 실제 NULL 값과 소계용 NULL 값을 구분할 수 없습니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UNION을 이용한 소계 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;WITH ROLLUP&lt;/code&gt;이 MySQL 5.0 이상에서 지원되지만, 더 세밀한 제어가 필요하거나 이전 버전을 사용해야 하는 경우 &lt;code&gt;UNION&lt;/code&gt;을 활용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 제품별 판매량
SELECT category, product, SUM(sales) AS total_sales
FROM sales_data
GROUP BY category, product

UNION ALL

-- 카테고리별 판매량
SELECT category, '모든 제품' AS product, SUM(sales) AS total_sales
FROM sales_data
GROUP BY category

UNION ALL

-- 전체 판매량
SELECT '모든 카테고리' AS category, '모든 제품' AS product, SUM(sales) AS total_sales
FROM sales_data;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 코드가 길어지지만, 레이블을 더 명확하게 지정할 수 있고 소계 행의 순서를 더 잘 제어할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다중 레벨 소계와 순서 제어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 분석에서는 여러 레벨의 소계가 필요할 수 있습니다. 이런 경우 &lt;code&gt;ORDER BY&lt;/code&gt; 절을 추가하여 결과의 순서를 제어할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    IF(GROUPING(year) = 1, '모든 연도', year) AS year,
    IF(GROUPING(quarter) = 1, '모든 분기', quarter) AS quarter,
    IF(GROUPING(month) = 1, '모든 월', month) AS month,
    SUM(sales) AS total_sales
FROM 
    sales_data
GROUP BY 
    year, quarter, month
WITH ROLLUP
ORDER BY 
    year, quarter, month;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 연도, 분기, 월별로 데이터를 집계하고, 각 레벨에서 소계를 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무 활용 예시: 판매 데이터 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 비즈니스 환경에서 소계 쿼리가 어떻게 활용되는지 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    IF(GROUPING(region) = 1, '전체 지역', region) AS region,
    IF(GROUPING(product_category) = 1, '전체 카테고리', product_category) AS category,
    SUM(sales_amount) AS total_sales,
    ROUND(AVG(sales_amount), 2) AS avg_sales,
    COUNT(DISTINCT customer_id) AS customer_count
FROM 
    sales_transactions
WHERE 
    transaction_date BETWEEN '2024-01-01' AND '2024-03-31'
GROUP BY 
    region, product_category
WITH ROLLUP
ORDER BY 
    GROUPING(region), 
    region, 
    GROUPING(product_category), 
    product_category;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 지역별, 제품 카테고리별로 매출 합계, 평균 매출, 고객 수를 계산하고 소계를 제공합니다. &lt;code&gt;ORDER BY&lt;/code&gt; 절에서 &lt;code&gt;GROUPING()&lt;/code&gt; 함수를 사용하여 소계 행이 관련 그룹 바로 아래에 표시되도록 정렬합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;피벗 테이블과 소계 결합하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 분석에서 행과 열을 교차시켜 보여주는 피벗 테이블은 매우 유용합니다. MySQL에서는 &lt;code&gt;CASE&lt;/code&gt; 표현식을 사용하여 피벗 테이블을 구현할 수 있으며, 이를 소계 쿼리와 결합할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT 
    IF(GROUPING(product_category) = 1, '총계', product_category) AS category,
    SUM(CASE WHEN quarter = 'Q1' THEN sales_amount ELSE 0 END) AS Q1_sales,
    SUM(CASE WHEN quarter = 'Q2' THEN sales_amount ELSE 0 END) AS Q2_sales,
    SUM(CASE WHEN quarter = 'Q3' THEN sales_amount ELSE 0 END) AS Q3_sales,
    SUM(CASE WHEN quarter = 'Q4' THEN sales_amount ELSE 0 END) AS Q4_sales,
    SUM(sales_amount) AS yearly_total
FROM 
    sales_data
WHERE 
    year = 2024
GROUP BY 
    product_category
WITH ROLLUP;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 쿼리는 제품 카테고리별로 분기별 매출과 연간 총매출을 보여주는 피벗 테이블을 생성하고, 마지막에 전체 합계 행을 추가합니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;category&lt;/th&gt;
&lt;th&gt;Q1_sales&lt;/th&gt;
&lt;th&gt;Q2_sales&lt;/th&gt;
&lt;th&gt;Q3_sales&lt;/th&gt;
&lt;th&gt;Q4_sales&lt;/th&gt;
&lt;th&gt;yearly_total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;전자기기&lt;/td&gt;
&lt;td&gt;125000&lt;/td&gt;
&lt;td&gt;138000&lt;/td&gt;
&lt;td&gt;142000&lt;/td&gt;
&lt;td&gt;185000&lt;/td&gt;
&lt;td&gt;590000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가구&lt;/td&gt;
&lt;td&gt;84000&lt;/td&gt;
&lt;td&gt;92000&lt;/td&gt;
&lt;td&gt;87000&lt;/td&gt;
&lt;td&gt;110000&lt;/td&gt;
&lt;td&gt;373000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;의류&lt;/td&gt;
&lt;td&gt;67000&lt;/td&gt;
&lt;td&gt;75000&lt;/td&gt;
&lt;td&gt;82000&lt;/td&gt;
&lt;td&gt;115000&lt;/td&gt;
&lt;td&gt;339000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총계&lt;/td&gt;
&lt;td&gt;276000&lt;/td&gt;
&lt;td&gt;305000&lt;/td&gt;
&lt;td&gt;311000&lt;/td&gt;
&lt;td&gt;410000&lt;/td&gt;
&lt;td&gt;1302000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능 고려사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소계 쿼리는 데이터량이 많을 경우 성능에 영향을 줄 수 있습니다. 최적화를 위한 몇 가지 팁은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;필요한 열만 선택하여 쿼리 처리 속도를 향상시킵니다.&lt;/li&gt;
&lt;li&gt;적절한 인덱스를 사용하여 GROUP BY 연산을 최적화합니다.&lt;/li&gt;
&lt;li&gt;대용량 데이터의 경우, 임시 테이블을 사용하거나 미리 집계된 데이터를 준비할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXPLAIN&lt;/code&gt; 명령을 사용하여 쿼리 실행 계획을 분석하고 병목 현상을 파악합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;warn&quot;&gt;&lt;b&gt;주의:&lt;/b&gt; &lt;code&gt;WITH ROLLUP&lt;/code&gt;을 사용한 쿼리는 임시 테이블을 생성할 가능성이 높으며, 대용량 데이터에서는 메모리 사용량이 증가할 수 있습니다. 대규모 데이터셋에서는 성능 테스트를 진행하고 필요에 따라 쿼리를 최적화하는 것이 중요합니다.&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 소계 쿼리는 데이터 분석과 보고서 작성에 필수적인 도구입니다. &lt;code&gt;WITH ROLLUP&lt;/code&gt;, &lt;code&gt;GROUPING()&lt;/code&gt; 함수, 또는 &lt;code&gt;UNION&lt;/code&gt; 접근 방식을 통해 다양한 레벨에서의 집계 정보를 얻을 수 있습니다. 이를 통해 의사 결정에 필요한 다양한 각도에서의 데이터 인사이트를 한 번의 쿼리로 얻을 수 있어, 업무 효율성과 데이터 분석의 품질을 크게 향상시킬 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;tip&quot;&gt;&lt;b&gt;추가 학습 자료:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 공식 문서의 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/group-by-modifiers.html&quot;&gt;GROUP BY 수정자&lt;/a&gt; 섹션&lt;/li&gt;
&lt;li&gt;MySQL 8.0의 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/aggregate-functions.html#function_grouping&quot;&gt;GROUPING() 함수&lt;/a&gt; 문서&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description>
      <category>DB/MySql</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/120</guid>
      <comments>https://devsite.tistory.com/entry/MySQL-%EC%86%8C%EA%B3%84%EC%BF%BC%EB%A6%AC%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%9C-%EB%8F%84%EA%B5%AC#entry120comment</comments>
      <pubDate>Sun, 11 May 2025 20:21:03 +0900</pubDate>
    </item>
    <item>
      <title>[DB] MySQL, MSSQL, Oracle 날짜 함수 비교표</title>
      <link>https://devsite.tistory.com/entry/DB-MySQL-MSSQL-Oracle-%EB%82%A0%EC%A7%9C-%ED%95%A8%EC%88%98-%EB%B9%84%EA%B5%90%ED%91%9C</link>
      <description>&lt;table data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;기능&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;MSSQL&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;현재 날짜 및 시간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;NOW(), CURRENT_TIMESTAMP()&lt;/td&gt;
&lt;td&gt;GETDATE(), CURRENT_TIMESTAMP&lt;/td&gt;
&lt;td&gt;SYSDATE, CURRENT_DATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;현재 날짜만&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CURDATE(), CURRENT_DATE()&lt;/td&gt;
&lt;td&gt;CONVERT(date, GETDATE())&lt;/td&gt;
&lt;td&gt;TRUNC(SYSDATE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;현재 시간만&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CURTIME(), CURRENT_TIME()&lt;/td&gt;
&lt;td&gt;CONVERT(time, GETDATE())&lt;/td&gt;
&lt;td&gt;TO_CHAR(SYSDATE, 'HH24:MI:SS')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 형식 변환&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DATE_FORMAT(date, format)&lt;/td&gt;
&lt;td&gt;CONVERT(varchar, date, format_code)&lt;/td&gt;
&lt;td&gt;TO_CHAR(date, format)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;문자열을 날짜로 변환&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;STR_TO_DATE(string, format)&lt;/td&gt;
&lt;td&gt;CONVERT(datetime, string, format_code)&lt;/td&gt;
&lt;td&gt;TO_DATE(string, format)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 더하기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DATE_ADD(date, INTERVAL value unit)&lt;/td&gt;
&lt;td&gt;DATEADD(unit, value, date)&lt;/td&gt;
&lt;td&gt;date + value (일 단위)&lt;br /&gt;ADD_MONTHS(date, value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 빼기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DATE_SUB(date, INTERVAL value unit)&lt;/td&gt;
&lt;td&gt;DATEADD(unit, -value, date)&lt;/td&gt;
&lt;td&gt;date - value (일 단위)&lt;br /&gt;ADD_MONTHS(date, -value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 간 차이&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DATEDIFF(date1, date2) (일 단위)&lt;/td&gt;
&lt;td&gt;DATEDIFF(unit, date1, date2)&lt;/td&gt;
&lt;td&gt;date1 - date2 (일 단위)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 추출 (연도)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;YEAR(date)&lt;/td&gt;
&lt;td&gt;YEAR(date), DATEPART(year, date)&lt;/td&gt;
&lt;td&gt;EXTRACT(YEAR FROM date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 추출 (월)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;MONTH(date)&lt;/td&gt;
&lt;td&gt;MONTH(date), DATEPART(month, date)&lt;/td&gt;
&lt;td&gt;EXTRACT(MONTH FROM date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 추출 (일)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DAY(date)&lt;/td&gt;
&lt;td&gt;DAY(date), DATEPART(day, date)&lt;/td&gt;
&lt;td&gt;EXTRACT(DAY FROM date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 추출 (시간)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;HOUR(date)&lt;/td&gt;
&lt;td&gt;DATEPART(hour, date)&lt;/td&gt;
&lt;td&gt;EXTRACT(HOUR FROM date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜의 마지막 일&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;LAST_DAY(date)&lt;/td&gt;
&lt;td&gt;EOMONTH(date)&lt;/td&gt;
&lt;td&gt;LAST_DAY(date)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;요일 추출&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DAYNAME(date), DAYOFWEEK(date)&lt;/td&gt;
&lt;td&gt;DATENAME(weekday, date)&lt;/td&gt;
&lt;td&gt;TO_CHAR(date, 'Day')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 반올림&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;ROUND(date, format)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;날짜 자르기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DATE(datetime)&lt;/td&gt;
&lt;td&gt;CAST(datetime AS DATE)&lt;/td&gt;
&lt;td&gt;TRUNC(date, format)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;분기 추출&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;QUARTER(date)&lt;/td&gt;
&lt;td&gt;DATEPART(quarter, date)&lt;/td&gt;
&lt;td&gt;TO_CHAR(date, 'Q')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;해당 연도의 몇 번째 날&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DAYOFYEAR(date)&lt;/td&gt;
&lt;td&gt;DATEPART(dayofyear, date)&lt;/td&gt;
&lt;td&gt;TO_CHAR(date, 'DDD')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;해당 주의 몇 번째 날&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;DAYOFWEEK(date)&lt;/td&gt;
&lt;td&gt;DATEPART(weekday, date)&lt;/td&gt;
&lt;td&gt;TO_CHAR(date, 'D')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;타임존 변환&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;CONVERT_TZ(date, from_tz, to_tz)&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;FROM_TZ, NEW_TIME&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;형식 지정 예시&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MySQL 날짜 형식&lt;/h3&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;SELECT DATE_FORMAT('2023-01-15', '%Y년 %m월 %d일'); -- 2023년 01월 15일&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSSQL 날짜 형식&lt;/h3&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT CONVERT(varchar, '2023-01-15', 111); -- 2023/01/15
SELECT FORMAT(CAST('2023-01-15' AS datetime), 'yyyy년 MM월 dd일'); -- SQL Server 2012 이상&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Oracle 날짜 형식&lt;/h3&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;SELECT TO_CHAR(TO_DATE('2023-01-15', 'YYYY-MM-DD'), 'YYYY&quot;년&quot; MM&quot;월&quot; DD&quot;일&quot;') FROM dual; -- 2023년 01월 15일&lt;/code&gt;&lt;/pre&gt;</description>
      <category>DB</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/119</guid>
      <comments>https://devsite.tistory.com/entry/DB-MySQL-MSSQL-Oracle-%EB%82%A0%EC%A7%9C-%ED%95%A8%EC%88%98-%EB%B9%84%EA%B5%90%ED%91%9C#entry119comment</comments>
      <pubDate>Mon, 10 Mar 2025 21:41:49 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Java Stream의 병렬처리: 성능 향상의 비밀</title>
      <link>https://devsite.tistory.com/entry/JAVA-Java-Stream%EC%9D%98-%EB%B3%91%EB%A0%AC%EC%B2%98%EB%A6%AC-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%9D%98-%EB%B9%84%EB%B0%80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;8a8ebe8c-c8b1-49af-9534-24a6584a5af5.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/01aw2/btsMzulCBBC/hKjEnKmA6OouYmPgwghJxk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/01aw2/btsMzulCBBC/hKjEnKmA6OouYmPgwghJxk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/01aw2/btsMzulCBBC/hKjEnKmA6OouYmPgwghJxk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F01aw2%2FbtsMzulCBBC%2FhKjEnKmA6OouYmPgwghJxk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;321&quot; height=&quot;321&quot; data-filename=&quot;8a8ebe8c-c8b1-49af-9534-24a6584a5af5.webp&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 8에서 도입된 Stream API는 데이터 처리 방식에 혁신을 가져왔습니다. 특히 &lt;b&gt;병렬 스트림(Parallel Stream)&lt;/b&gt; 기능은 멀티코어 프로세서의 성능을 최대한 활용할 수 있게 해주는 강력한 도구입니다. 현대 애플리케이션에서 대용량 데이터 처리가 일상화된 지금, 병렬 처리의 중요성은 더욱 커지고 있습니다. 과연 병렬 스트림은 어떤 상황에서 효과적일까요? 일반 스트림과 비교해 얼마나 성능 향상을 가져올 수 있을까요?&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 병렬 스트림의 기본 개념&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림은 데이터를 여러 청크(chunk)로 분할하여 각각 다른 스레드에서 처리한 후 결과를 합치는 방식으로 작동합니다. Java의 &lt;b&gt;Fork/Join 프레임워크&lt;/b&gt;를 기반으로 하여 복잡한 멀티스레드 프로그래밍 없이도 간단하게 병렬 처리를 구현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 스트림과 병렬 스트림의 가장 큰 차이점은 코드 한 줄의 차이로 나타납니다:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 일반 스트림
List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.stream()
                .filter(n -&amp;gt; n % 2 == 0)
                .mapToInt(Integer::intValue)
                .sum();

// 병렬 스트림
int parallelSum = numbers.parallelStream()
                        .filter(n -&amp;gt; n % 2 == 0)
                        .mapToInt(Integer::intValue)
                        .sum();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서 볼 수 있듯이, 일반 &lt;code&gt;stream()&lt;/code&gt;을 &lt;code&gt;parallelStream()&lt;/code&gt;으로 변경하거나 &lt;code&gt;.parallel()&lt;/code&gt; 메서드를 호출하는 것만으로 병렬 처리가 가능해집니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 병렬 스트림의 내부 작동 원리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림이 어떻게 작동하는지 이해하기 위해서는 &lt;b&gt;Fork/Join 프레임워크&lt;/b&gt;에 대한 기본 지식이 필요합니다. 이 프레임워크는 작업을 재귀적으로 작은 단위로 분할하고(fork), 각 단위 작업의 결과를 합치는(join) 방식으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림의 처리 과정은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터 소스를 여러 청크로 분할&lt;/li&gt;
&lt;li&gt;각 청크를 별도의 스레드에서 처리&lt;/li&gt;
&lt;li&gt;처리된 결과를 합치기&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 큰 배열의 모든 요소를 제곱하는 작업을 병렬로 처리한다고 가정해 보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;int[] numbers = IntStream.rangeClosed(1, 10_000_000).toArray();

// 순차 처리
long startTime1 = System.currentTimeMillis();
Arrays.stream(numbers)
      .map(n -&amp;gt; n * n)
      .sum();
long endTime1 = System.currentTimeMillis();

// 병렬 처리
long startTime2 = System.currentTimeMillis();
Arrays.stream(numbers)
      .parallel()
      .map(n -&amp;gt; n * n)
      .sum();
long endTime2 = System.currentTimeMillis();

System.out.println(&quot;순차 처리 시간: &quot; + (endTime1 - startTime1) + &quot;ms&quot;);
System.out.println(&quot;병렬 처리 시간: &quot; + (endTime2 - startTime2) + &quot;ms&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제를 실행하면 대부분의 현대 컴퓨터에서 병렬 처리가 순차 처리보다 훨씬 빠르게 완료됩니다. 8코어 CPU에서는 이론적으로 최대 8배의 성능 향상이 가능합니다(실제로는 오버헤드로 인해 그보다 낮은 향상을 보입니다).&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 병렬 스트림의 적절한 사용 시나리오&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 상황에서 병렬 스트림이 좋은 성능을 보장하지는 않습니다. 병렬 처리는 다음과 같은 경우에 효과적입니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 크기가 충분히 큰 경우&lt;/b&gt;: 작은 데이터셋에서는 병렬화 오버헤드가 성능 이득보다 클 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작업이 계산 집약적인 경우&lt;/b&gt;: 간단한 작업보다 복잡한 계산이 필요한 작업에서 더 효과적입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 소스가 쉽게 분할 가능한 경우&lt;/b&gt;: ArrayList와 같은 자료구조는 효율적으로 분할되지만, LinkedList는 그렇지 않습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 대용량 데이터에서 소수를 찾는 예제입니다:&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;long n = 10_000_000;

// 순차 처리
long startTime1 = System.currentTimeMillis();
long count1 = LongStream.rangeClosed(2, n)
                      .filter(ParallelStreamDemo::isPrime)
                      .count();
long endTime1 = System.currentTimeMillis();

// 병렬 처리
long startTime2 = System.currentTimeMillis();
long count2 = LongStream.rangeClosed(2, n)
                      .parallel()
                      .filter(ParallelStreamDemo::isPrime)
                      .count();
long endTime2 = System.currentTimeMillis();

System.out.println(&quot;소수 개수: &quot; + count1);
System.out.println(&quot;순차 처리 시간: &quot; + (endTime1 - startTime1) + &quot;ms&quot;);
System.out.println(&quot;병렬 처리 시간: &quot; + (endTime2 - startTime2) + &quot;ms&quot;);

// 소수 판별 메서드
public static boolean isPrime(long n) {
    if (n &amp;lt;= 1) return false;
    if (n &amp;lt;= 3) return true;
    if (n % 2 == 0 || n % 3 == 0) return false;

    for (long i = 5; i * i &amp;lt;= n; i += 6) {
        if (n % i == 0 || n % (i + 2) == 0) return false;
    }
    return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는 소수 판별이라는 계산 집약적인 작업과 큰 데이터셋을 사용하기 때문에 병렬 처리의 이점이 뚜렷하게 나타납니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 병렬 스트림 사용 시 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림을 사용할 때는 다음과 같은 주의사항을 고려해야 합니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;상태 공유 최소화&lt;/b&gt;: 여러 스레드가 동시에 접근하는 상태 변수는 경쟁 조건(race condition)을 유발할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;// 잘못된 예제
List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int[] sum = {0}; // 공유 상태
numbers.parallelStream().forEach(n -&amp;gt; sum[0] += n); // 경쟁 조건 발생!
System.out.println(&quot;합계: &quot; + sum[0]); // 결과가 일관되지 않을 수 있음

// 올바른 예제
int correctSum = numbers.parallelStream().reduce(0, Integer::sum);
System.out.println(&quot;올바른 합계: &quot; + correctSum);&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;순서 의존적 연산 주의&lt;/b&gt;: &lt;code&gt;findFirst()&lt;/code&gt;, &lt;code&gt;limit()&lt;/code&gt; 같은 순서에 의존하는 연산은 병렬 처리에서 성능이 저하될 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 순서 의존적 연산의 예
List&amp;lt;Integer&amp;gt; numbers = IntStream.rangeClosed(1, 1_000_000).boxed().collect(Collectors.toList());

// 병렬 스트림에서 순서 의존적 연산은 비효율적일 수 있음
Optional&amp;lt;Integer&amp;gt; firstEven = numbers.parallelStream()
                                 .filter(n -&amp;gt; n % 2 == 0)
                                 .findFirst(); // 병렬 처리에서 비효율적&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;결정적 연산자 사용&lt;/b&gt;: 병렬 처리에서는 연산의 순서가 보장되지 않으므로, 비결정적 연산(예: &lt;code&gt;forEach&lt;/code&gt;)보다 결정적 연산(&lt;code&gt;reduce&lt;/code&gt;, &lt;code&gt;collect&lt;/code&gt; 등)을 사용하는 것이 안전합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 비결정적 연산의 예
List&amp;lt;String&amp;gt; names = Arrays.asList(&quot;Tom&quot;, &quot;Jerry&quot;, &quot;Mickey&quot;, &quot;Donald&quot;);
names.parallelStream().forEach(System.out::println); // 출력 순서가 매번 다를 수 있음

// 결정적 연산의 예
List&amp;lt;String&amp;gt; sortedNames = names.parallelStream()
                             .sorted()
                             .collect(Collectors.toList()); // 결과는 항상 동일&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 성능 측정 및 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림의 실제 성능 이점을 확인하려면 정확한 성능 측정이 필요합니다. Java의 &lt;b&gt;JMH(Java Microbenchmark Harness)&lt;/b&gt;를 사용하면 보다 정확한 성능 테스트가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 간단한 성능 측정 예제입니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;Integer&amp;gt; numbers = IntStream.rangeClosed(1, 10_000_000).boxed().collect(Collectors.toList());

// 워밍업 (JIT 컴파일러 최적화를 위함)
for (int i = 0; i &amp;lt; 5; i++) {
    numbers.stream().reduce(0, Integer::sum);
    numbers.parallelStream().reduce(0, Integer::sum);
}

// 성능 측정
long start1 = System.nanoTime();
long sum1 = numbers.stream().reduce(0, Integer::sum);
long duration1 = (System.nanoTime() - start1) / 1_000_000;

long start2 = System.nanoTime();
long sum2 = numbers.parallelStream().reduce(0, Integer::sum);
long duration2 = (System.nanoTime() - start2) / 1_000_000;

System.out.println(&quot;순차 스트림: &quot; + duration1 + &quot;ms&quot;);
System.out.println(&quot;병렬 스트림: &quot; + duration2 + &quot;ms&quot;);
System.out.println(&quot;속도 향상: &quot; + (double)duration1 / duration2 + &quot;배&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 측정을 통해 실제 애플리케이션에서 병렬 처리가 얼마나 효과적인지 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 실제 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림은 다양한 실제 시나리오에서 활용될 수 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;대용량 파일 처리&lt;/b&gt;: 대용량 로그 파일을 분석하거나 변환할 때 유용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;// 대용량 파일의 각 라인을 병렬로 처리
Path filePath = Paths.get(&quot;huge_log_file.txt&quot;);
try {
    long errorCount = Files.lines(filePath)
                        .parallel()
                        .filter(line -&amp;gt; line.contains(&quot;ERROR&quot;))
                        .count();
    System.out.println(&quot;에러 발생 횟수: &quot; + errorCount);
} catch (IOException e) {
    e.printStackTrace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 변환 작업&lt;/b&gt;: 대량의 데이터를 한 형식에서 다른 형식으로 변환할 때 효과적입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 사용자 객체 리스트를 DTO로 변환
List&amp;lt;User&amp;gt; users = userRepository.findAll(); // 수천 개의 사용자 데이터
List&amp;lt;UserDTO&amp;gt; userDTOs = users.parallelStream()
                          .map(user -&amp;gt; convertToDTO(user))
                          .collect(Collectors.toList());

// 변환 메서드 (계산 비용이 높다고 가정)
private UserDTO convertToDTO(User user) {
    // 복잡한 변환 로직
    return new UserDTO(user.getId(), user.getName(), calculateRating(user));
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;이미지 처리&lt;/b&gt;: 대량의 이미지를 리사이징하거나 필터링하는 작업을 병렬화할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 실제 사례들에서 병렬 스트림은 상당한 성능 향상을 가져올 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java Stream의 병렬 처리 기능은 멀티코어 환경에서 애플리케이션의 성능을 크게 향상시킬 수 있는 강력한 도구입니다. 적절한 사용 사례를 선택하고 주의사항을 고려한다면, 최소한의 코드 변경으로 최대의 성능 이득을 얻을 수 있습니다. 앞으로 하드웨어의 다중 코어 확장 추세에 따라 병렬 프로그래밍의 중요성은 더욱 커질 것입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;[전문용어]&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[Stream API]: Java 8에서 도입된 컬렉션 데이터를 함수형으로 처리할 수 있는 기능&lt;/li&gt;
&lt;li&gt;[병렬 스트림]: 데이터를 여러 스레드에서 병렬로 처리하는 스트림&lt;/li&gt;
&lt;li&gt;[Fork/Join 프레임워크]: 재귀적으로 작업을 분할하고 결과를 합치는 병렬 처리 프레임워크&lt;/li&gt;
&lt;li&gt;[경쟁 조건]: 여러 스레드가 공유 데이터에 동시에 접근하여 발생하는 버그&lt;/li&gt;
&lt;li&gt;[JIT 컴파일러]: 자바 바이트코드를 런타임에 기계어로 변환하는 컴파일러&lt;/li&gt;
&lt;li&gt;[JMH]: Java Microbenchmark Harness, 자바 코드의 성능을 정밀하게 측정하는 도구&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <category>JavaStream</category>
      <category>대용량데이터처리</category>
      <category>멀티스레딩</category>
      <category>병령처리</category>
      <category>자바8</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/118</guid>
      <comments>https://devsite.tistory.com/entry/JAVA-Java-Stream%EC%9D%98-%EB%B3%91%EB%A0%AC%EC%B2%98%EB%A6%AC-%EC%84%B1%EB%8A%A5-%ED%96%A5%EC%83%81%EC%9D%98-%EB%B9%84%EB%B0%80#entry118comment</comments>
      <pubDate>Thu, 27 Feb 2025 06:45:22 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Java Stream API 완벽 가이드 - Part 5: 심화 학습과 실전 활용</title>
      <link>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-5-%EC%8B%AC%ED%99%94-%ED%95%99%EC%8A%B5%EA%B3%BC-%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 커스텀 Stream 구현&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Spliterator 이해하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spliterator는 Java 8에서 도입된 인터페이스로, 컬렉션의 요소를 분할하고 순회하는 기능을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class CustomSpliterator&amp;lt;T&amp;gt; implements Spliterator&amp;lt;T&amp;gt; {
    private final List&amp;lt;T&amp;gt; list;
    private int current = 0;

    public CustomSpliterator(List&amp;lt;T&amp;gt; list) {
        this.list = list;
    }

    @Override
    public boolean tryAdvance(Consumer&amp;lt;? super T&amp;gt; action) {
        if (current &amp;lt; list.size()) {
            action.accept(list.get(current++));
            return true;
        }
        return false;
    }

    @Override
    public Spliterator&amp;lt;T&amp;gt; trySplit() {
        int currentSize = list.size() - current;
        if (currentSize &amp;lt;= 1) {
            return null;
        }

        int splitPoint = current + currentSize / 2;
        List&amp;lt;T&amp;gt; splitList = list.subList(current, splitPoint);
        current = splitPoint;
        return new CustomSpliterator&amp;lt;&amp;gt;(splitList);
    }

    @Override
    public long estimateSize() {
        return list.size() - current;
    }

    @Override
    public int characteristics() {
        return ORDERED | SIZED | SUBSIZED;
    }
}

// 사용 예제
public class TimeSeriesStream {
    public static Stream&amp;lt;TimeSeriesData&amp;gt; createStream(List&amp;lt;TimeSeriesData&amp;gt; data) {
        return StreamSupport.stream(
            new CustomSpliterator&amp;lt;&amp;gt;(data), true);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 커스텀 Collector 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 집계 요구사항을 처리하기 위한 커스텀 Collector입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;public class WeightedAverageCollector implements 
        Collector&amp;lt;Transaction, WeightedAverageCollector.Accumulator, Double&amp;gt; {

    static class Accumulator {
        private double sum = 0;
        private double weightSum = 0;

        void add(double value, double weight) {
            sum += value * weight;
            weightSum += weight;
        }
    }

    @Override
    public Supplier&amp;lt;Accumulator&amp;gt; supplier() {
        return Accumulator::new;
    }

    @Override
    public BiConsumer&amp;lt;Accumulator, Transaction&amp;gt; accumulator() {
        return (acc, transaction) -&amp;gt; 
            acc.add(transaction.getAmount().doubleValue(), 
                   calculateWeight(transaction));
    }

    @Override
    public BinaryOperator&amp;lt;Accumulator&amp;gt; combiner() {
        return (acc1, acc2) -&amp;gt; {
            Accumulator combined = new Accumulator();
            combined.sum = acc1.sum + acc2.sum;
            combined.weightSum = acc1.weightSum + acc2.weightSum;
            return combined;
        };
    }

    @Override
    public Function&amp;lt;Accumulator, Double&amp;gt; finisher() {
        return acc -&amp;gt; acc.sum / acc.weightSum;
    }

    @Override
    public Set&amp;lt;Characteristics&amp;gt; characteristics() {
        return Collections.emptySet();
    }

    private double calculateWeight(Transaction transaction) {
        // 가중치 계산 로직 구현
        return transaction.getImportance().getValue();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 고급 스트림 활용 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 동적 필터링 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 조건을 조합하여 동적으로 필터를 생성하는 패턴입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class DynamicFilter {
    public class FilterCriteria {
        private LocalDate startDate;
        private LocalDate endDate;
        private BigDecimal minAmount;
        private Set&amp;lt;String&amp;gt; categories;
        // getter/setter 생략
    }

    public List&amp;lt;Transaction&amp;gt; filterTransactions(
            List&amp;lt;Transaction&amp;gt; transactions, 
            FilterCriteria criteria) {

        Predicate&amp;lt;Transaction&amp;gt; datePredicate = criteria.getStartDate() != null ?
            t -&amp;gt; !t.getDate().isBefore(criteria.getStartDate()) : t -&amp;gt; true;

        Predicate&amp;lt;Transaction&amp;gt; amountPredicate = criteria.getMinAmount() != null ?
            t -&amp;gt; t.getAmount().compareTo(criteria.getMinAmount()) &amp;gt;= 0 : t -&amp;gt; true;

        Predicate&amp;lt;Transaction&amp;gt; categoryPredicate = 
            criteria.getCategories() != null &amp;amp;&amp;amp; !criteria.getCategories().isEmpty() ?
            t -&amp;gt; criteria.getCategories().contains(t.getCategory()) : t -&amp;gt; true;

        return transactions.stream()
            .filter(datePredicate)
            .filter(amountPredicate)
            .filter(categoryPredicate)
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 재귀적 스트림 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리 구조나 계층 구조의 데이터를 처리하는 패턴입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class TreeProcessor {
    public class TreeNode {
        private String value;
        private List&amp;lt;TreeNode&amp;gt; children;
        // getter/setter 생략
    }

    public Stream&amp;lt;TreeNode&amp;gt; flattenTree(TreeNode root) {
        return Stream.concat(
            Stream.of(root),
            root.getChildren().stream()
                .flatMap(this::flattenTree)
        );
    }

    // 특정 깊이까지만 처리
    public Stream&amp;lt;TreeNode&amp;gt; flattenTreeWithDepth(TreeNode root, int maxDepth) {
        if (maxDepth == 0) {
            return Stream.of(root);
        }

        return Stream.concat(
            Stream.of(root),
            root.getChildren().stream()
                .flatMap(child -&amp;gt; flattenTreeWithDepth(child, maxDepth - 1))
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 실전 시나리오&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 대량 데이터 처리 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 데이터를 효율적으로 처리하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class BigDataProcessor {
    private static final int BATCH_SIZE = 1000;

    public void processBigData(Stream&amp;lt;DataRecord&amp;gt; dataStream) {
        AtomicInteger counter = new AtomicInteger();

        List&amp;lt;DataRecord&amp;gt; batchBuffer = dataStream
            .collect(Collectors.groupingBy(
                record -&amp;gt; counter.getAndIncrement() / BATCH_SIZE
            ))
            .values()
            .stream()
            .parallel()
            .map(this::processBatch)
            .flatMap(Collection::stream)
            .collect(Collectors.toList());
    }

    private List&amp;lt;DataRecord&amp;gt; processBatch(List&amp;lt;DataRecord&amp;gt; batch) {
        return batch.stream()
            .map(this::enrichData)
            .filter(this::validateData)
            .map(this::transformData)
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 실시간 분석 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 들어오는 데이터를 분석하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class RealTimeAnalyzer {
    private final Queue&amp;lt;Transaction&amp;gt; recentTransactions = 
        new ConcurrentLinkedQueue&amp;lt;&amp;gt;();
    private final int windowSize = 1000;

    public void processTransaction(Transaction transaction) {
        recentTransactions.add(transaction);

        // 윈도우 크기 유지
        while (recentTransactions.size() &amp;gt; windowSize) {
            recentTransactions.poll();
        }

        // 실시간 통계 계산
        DoubleSummaryStatistics stats = recentTransactions.stream()
            .mapToDouble(t -&amp;gt; t.getAmount().doubleValue())
            .summaryStatistics();

        // 이상 거래 탐지
        double average = stats.getAverage();
        double stdDev = calculateStandardDeviation(
            recentTransactions.stream(), average);

        if (isAnomalous(transaction, average, stdDev)) {
            raiseAlert(transaction);
        }
    }

    private double calculateStandardDeviation(
            Stream&amp;lt;Transaction&amp;gt; stream, double mean) {
        return Math.sqrt(stream
            .mapToDouble(t -&amp;gt; {
                double diff = t.getAmount().doubleValue() - mean;
                return diff * diff;
            })
            .average()
            .orElse(0.0));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 성능 최적화와 모니터링&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 성능 모니터링 시스템 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림 연산의 성능을 모니터링하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class StreamPerformanceMonitor {
    private static final Logger logger = 
        LoggerFactory.getLogger(StreamPerformanceMonitor.class);

    public &amp;lt;T, R&amp;gt; R measureStreamPerformance(
            Stream&amp;lt;T&amp;gt; stream, 
            Function&amp;lt;Stream&amp;lt;T&amp;gt;, R&amp;gt; streamOperation,
            String operationName) {

        long startTime = System.nanoTime();
        R result = streamOperation.apply(stream);
        long endTime = System.nanoTime();

        long duration = (endTime - startTime) / 1_000_000; // 밀리초 변환

        logger.info(&quot;Stream operation '{}' took {} ms&quot;, 
            operationName, duration);

        return result;
    }

    // 사용 예제
    public void example() {
        List&amp;lt;Transaction&amp;gt; transactions = getTransactions();

        Map&amp;lt;String, Double&amp;gt; result = measureStreamPerformance(
            transactions.stream(),
            stream -&amp;gt; stream
                .filter(t -&amp;gt; t.getAmount().compareTo(BigDecimal.ZERO) &amp;gt; 0)
                .collect(Collectors.groupingBy(
                    Transaction::getCategory,
                    Collectors.averagingDouble(t -&amp;gt; 
                        t.getAmount().doubleValue())
                )),
            &quot;Category averaging&quot;
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것으로 Stream API의 심화 학습을 마무리합니다. 이러한 고급 기능들을 활용하면 더 복잡한 비즈니스 요구사항도 효과적으로 처리할 수 있습니다.&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/117</guid>
      <comments>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-5-%EC%8B%AC%ED%99%94-%ED%95%99%EC%8A%B5%EA%B3%BC-%EC%8B%A4%EC%A0%84-%ED%99%9C%EC%9A%A9#entry117comment</comments>
      <pubDate>Sat, 18 Jan 2025 14:21:46 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Java Stream API 완벽 가이드 - Part 4: 테스트와 디버깅, 베스트 프랙티스</title>
      <link>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%99%80-%EB%94%94%EB%B2%84%EA%B9%85-%EB%B2%A0%EC%8A%A4%ED%8A%B8-%ED%94%84%EB%9E%99%ED%8B%B0%EC%8A%A4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Stream API 테스트 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 단위 테스트 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream API를 사용하는 코드의 효과적인 테스트 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;@Test
class SalesAnalyzerTest {

    public class SalesAnalyzer {
        public Map&amp;lt;String, SalesStats&amp;gt; analyzeSalesByCategory(List&amp;lt;Sale&amp;gt; sales) {
            return sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::getCategory,
                    Collectors.collectingAndThen(
                        Collectors.toList(),
                        this::calculateStats
                    )
                ));
        }

        private SalesStats calculateStats(List&amp;lt;Sale&amp;gt; sales) {
            return new SalesStats(
                sales.stream()
                    .map(Sale::getAmount)
                    .reduce(BigDecimal.ZERO, BigDecimal::add),
                sales.size()
            );
        }
    }

    @Test
    @DisplayName(&quot;카테고리별 판매 통계 계산 테스트&quot;)
    void analyzeSalesByCategory() {
        // Given
        List&amp;lt;Sale&amp;gt; sales = Arrays.asList(
            new Sale(&quot;전자제품&quot;, new BigDecimal(&quot;1000000&quot;)),
            new Sale(&quot;전자제품&quot;, new BigDecimal(&quot;1500000&quot;)),
            new Sale(&quot;의류&quot;, new BigDecimal(&quot;500000&quot;)),
            new Sale(&quot;의류&quot;, new BigDecimal(&quot;300000&quot;))
        );

        SalesAnalyzer analyzer = new SalesAnalyzer();

        // When
        Map&amp;lt;String, SalesStats&amp;gt; result = analyzer.analyzeSalesByCategory(sales);

        // Then
        assertAll(
            () -&amp;gt; assertEquals(2, result.size()),
            () -&amp;gt; assertEquals(
                new BigDecimal(&quot;2500000&quot;), 
                result.get(&quot;전자제품&quot;).getTotalAmount()
            ),
            () -&amp;gt; assertEquals(
                2, 
                result.get(&quot;전자제품&quot;).getCount()
            ),
            () -&amp;gt; assertEquals(
                new BigDecimal(&quot;800000&quot;), 
                result.get(&quot;의류&quot;).getTotalAmount()
            ),
            () -&amp;gt; assertEquals(
                2, 
                result.get(&quot;의류&quot;).getCount()
            )
        );
    }

    @Test
    @DisplayName(&quot;빈 리스트에 대한 처리 테스트&quot;)
    void analyzeSalesWithEmptyList() {
        // Given
        List&amp;lt;Sale&amp;gt; sales = Collections.emptyList();
        SalesAnalyzer analyzer = new SalesAnalyzer();

        // When
        Map&amp;lt;String, SalesStats&amp;gt; result = analyzer.analyzeSalesByCategory(sales);

        // Then
        assertTrue(result.isEmpty());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 복잡한 Stream 연산 테스트&lt;/h3&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;public class ComplexStreamProcessor {
    public List&amp;lt;TransactionSummary&amp;gt; processTransactions(
            List&amp;lt;Transaction&amp;gt; transactions) {
        return transactions.stream()
            .filter(this::isValidTransaction)
            .collect(Collectors.groupingBy(
                Transaction::getCustomerId,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    this::summarizeTransactions
                )))
            .values()
            .stream()
            .sorted(Comparator.comparing(TransactionSummary::getTotalAmount)
                .reversed())
            .collect(Collectors.toList());
    }

    @Test
    @DisplayName(&quot;복잡한 거래 처리 테스트&quot;)
    void testComplexTransactionProcessing() {
        // Given
        List&amp;lt;Transaction&amp;gt; transactions = createTestTransactions();
        ComplexStreamProcessor processor = new ComplexStreamProcessor();

        // When
        List&amp;lt;TransactionSummary&amp;gt; result = processor.processTransactions(transactions);

        // Then
        assertAll(
            // 결과 크기 확인
            () -&amp;gt; assertEquals(3, result.size()),

            // 정렬 순서 확인
            () -&amp;gt; assertTrue(
                result.get(0).getTotalAmount()
                    .compareTo(result.get(1).getTotalAmount()) &amp;gt;= 0
            ),

            // 특정 고객의 거래 요약 확인
            () -&amp;gt; {
                TransactionSummary firstSummary = result.get(0);
                assertEquals(&quot;CUST001&quot;, firstSummary.getCustomerId());
                assertEquals(new BigDecimal(&quot;1500000&quot;), 
                    firstSummary.getTotalAmount());
                assertEquals(3, firstSummary.getTransactionCount());
            }
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 디버깅 전략&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 peek()를 활용한 디버깅&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class StreamDebugger {
    private static final Logger log = LoggerFactory.getLogger(StreamDebugger.class);

    public List&amp;lt;ProcessedData&amp;gt; processWithDebugLogging(List&amp;lt;RawData&amp;gt; dataList) {
        return dataList.stream()
            .peek(data -&amp;gt; log.debug(&quot;Before filtering: {}&quot;, data))
            .filter(this::isValid)
            .peek(data -&amp;gt; log.debug(&quot;After filtering: {}&quot;, data))
            .map(this::transform)
            .peek(data -&amp;gt; log.debug(&quot;After transformation: {}&quot;, data))
            .filter(this::meetsCriteria)
            .peek(data -&amp;gt; log.debug(&quot;Final result: {}&quot;, data))
            .collect(Collectors.toList());
    }

    // 디버깅을 위한 중간 결과 수집기
    public static &amp;lt;T&amp;gt; Collector&amp;lt;T, ?, List&amp;lt;T&amp;gt;&amp;gt; debugCollector(String label) {
        return Collectors.collectingAndThen(
            Collectors.toList(),
            list -&amp;gt; {
                log.debug(&quot;{}: {}&quot;, label, list);
                return list;
            }
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 일반적인 문제와 해결 방법&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class CommonStreamIssues {
    // 문제 1: 스트림 재사용 시도
    public void streamReuseIssue() {
        Stream&amp;lt;String&amp;gt; stream = getDataStream();

        // 첫 번째 사용 - 정상
        List&amp;lt;String&amp;gt; list1 = stream.collect(Collectors.toList());

        // 두 번째 사용 - IllegalStateException 발생
        // List&amp;lt;String&amp;gt; list2 = stream.collect(Collectors.toList());

        // 해결방법: 새로운 스트림 생성
        List&amp;lt;String&amp;gt; list2 = getDataStream().collect(Collectors.toList());
    }

    // 문제 2: 무한 스트림 처리
    public void infiniteStreamIssue() {
        // 잘못된 방법
        // Stream.iterate(1, n -&amp;gt; n + 1)
        //     .collect(Collectors.toList()); // OutOfMemoryError 발생

        // 올바른 방법
        List&amp;lt;Integer&amp;gt; numbers = Stream.iterate(1, n -&amp;gt; n + 1)
            .limit(100)
            .collect(Collectors.toList());
    }

    // 문제 3: 병렬 스트림에서의 상태 공유
    public void parallelStreamStateIssue() {
        List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4, 5);

        // 잘못된 방법
        List&amp;lt;Integer&amp;gt; result1 = new ArrayList&amp;lt;&amp;gt;();
        numbers.parallelStream()
            .map(n -&amp;gt; n * 2)
            .forEach(result1::add); // 동시성 문제 발생

        // 올바른 방법
        List&amp;lt;Integer&amp;gt; result2 = numbers.parallelStream()
            .map(n -&amp;gt; n * 2)
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 베스트 프랙티스&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 코드 가독성 향상&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class StreamBestPractices {
    // 1. 적절한 메소드 추출
    public List&amp;lt;Employee&amp;gt; getHighPerformers(List&amp;lt;Employee&amp;gt; employees) {
        return employees.stream()
            .filter(this::isHighPerformer)
            .filter(this::isEligibleForPromotion)
            .sorted(this::compareByPerformance)
            .collect(Collectors.toList());
    }

    private boolean isHighPerformer(Employee emp) {
        return emp.getPerformanceScore() &amp;gt;= 4.5;
    }

    private boolean isEligibleForPromotion(Employee emp) {
        return emp.getYearsOfService() &amp;gt;= 2;
    }

    private int compareByPerformance(Employee e1, Employee e2) {
        return Double.compare(
            e2.getPerformanceScore(), 
            e1.getPerformanceScore()
        );
    }

    // 2. 복잡한 조건의 분리
    public List&amp;lt;Transaction&amp;gt; getValidTransactions(List&amp;lt;Transaction&amp;gt; transactions) {
        Predicate&amp;lt;Transaction&amp;gt; validAmount = t -&amp;gt; 
            t.getAmount().compareTo(BigDecimal.ZERO) &amp;gt; 0;

        Predicate&amp;lt;Transaction&amp;gt; validDate = t -&amp;gt; 
            t.getDate().isAfter(LocalDate.now().minusDays(30));

        Predicate&amp;lt;Transaction&amp;gt; validStatus = t -&amp;gt; 
            t.getStatus() != TransactionStatus.CANCELLED;

        return transactions.stream()
            .filter(validAmount.and(validDate).and(validStatus))
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 성능 고려사항&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class StreamPerformanceGuidelines {
    // 1. 적절한 데이터 구조 선택
    public Map&amp;lt;String, List&amp;lt;Transaction&amp;gt;&amp;gt; groupTransactions(
            Collection&amp;lt;Transaction&amp;gt; transactions) {
        // ArrayList보다 HashSet 사용이 검색에 효율적
        return new HashSet&amp;lt;&amp;gt;(transactions).stream()
            .collect(Collectors.groupingBy(Transaction::getCustomerId));
    }

    // 2. 불필요한 박싱/언박싱 피하기
    public double calculateAverage(List&amp;lt;Integer&amp;gt; numbers) {
        // 잘못된 방법
        // double avg1 = numbers.stream()
        //     .mapToDouble(Integer::doubleValue).average().orElse(0.0);

        // 올바른 방법
        return numbers.stream()
            .mapToInt(Integer::intValue)
            .average()
            .orElse(0.0);
    }

    // 3. 적절한 병렬화 결정
    public BigDecimal calculateTotalAmount(List&amp;lt;Order&amp;gt; orders) {
        return orders.size() &amp;gt; 10000 ?
            orders.parallelStream()
                .map(Order::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
            :
            orders.stream()
                .map(Order::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 실전 적용 가이드라인&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;스트림 체인의 적절한 길이 유지&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 긴 체인은 가독성을 해침&lt;/li&gt;
&lt;li&gt;중간 결과를 의미 있는 변수로 추출 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명확한 네이밍&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스트림 연산의 목적을 명확히 표현하는 메소드명 사용&lt;/li&gt;
&lt;li&gt;중간 결과를 저장하는 변수명도 의미있게 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예외 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스트림 내부의 예외는 try-catch로 감싸서 처리&lt;/li&gt;
&lt;li&gt;필요한 경우 Optional을 활용하여 null 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단위 테스트 작성&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 스트림 연산의 결과를 검증하는 테스트 케이스 작성&lt;/li&gt;
&lt;li&gt;경계 조건에 대한 테스트 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 모니터링&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 데이터 처리 시 성능 측정&lt;/li&gt;
&lt;li&gt;병렬 스트림 사용 여부 결정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 가이드라인을 따르면서 Stream API를 사용하면, 더 유지보수하기 쉽고 효율적인 코드를 작성할 수 있습니다.&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/116</guid>
      <comments>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-4-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%99%80-%EB%94%94%EB%B2%84%EA%B9%85-%EB%B2%A0%EC%8A%A4%ED%8A%B8-%ED%94%84%EB%9E%99%ED%8B%B0%EC%8A%A4#entry116comment</comments>
      <pubDate>Sat, 18 Jan 2025 14:19:36 +0900</pubDate>
    </item>
    <item>
      <title>[Java] Java Stream API 완벽 가이드 - Part 3: 고급 활용과 성능 최적화</title>
      <link>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-3-%EA%B3%A0%EA%B8%89-%ED%99%9C%EC%9A%A9%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 복잡한 데이터 처리 패턴&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 다중 조건 필터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 단순한 필터링이 아닌, 여러 조건을 조합해야 하는 경우가 많습니다. 이러한 경우 Stream API를 효과적으로 활용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class AdvancedFilterExample {
    public class SalesFilter {
        private LocalDateTime startDate;
        private LocalDateTime endDate;
        private Set&amp;lt;String&amp;gt; categories;
        private BigDecimal minAmount;
        private Set&amp;lt;String&amp;gt; excludedCustomers;
        // 생성자, getter, setter 생략
    }

    public List&amp;lt;Sale&amp;gt; filterSales(List&amp;lt;Sale&amp;gt; sales, SalesFilter filter) {
        return sales.stream()
            // 날짜 범위 필터
            .filter(sale -&amp;gt; isWithinDateRange(sale, filter))
            // 카테고리 필터
            .filter(sale -&amp;gt; filter.getCategories().isEmpty() || 
                filter.getCategories().contains(sale.getCategory()))
            // 최소 금액 필터
            .filter(sale -&amp;gt; sale.getAmount()
                .compareTo(filter.getMinAmount()) &amp;gt;= 0)
            // 제외 고객 필터
            .filter(sale -&amp;gt; !filter.getExcludedCustomers()
                .contains(sale.getCustomerId()))
            .collect(Collectors.toList());
    }

    private boolean isWithinDateRange(Sale sale, SalesFilter filter) {
        return !sale.getDateTime().isBefore(filter.getStartDate()) &amp;amp;&amp;amp;
               !sale.getDateTime().isAfter(filter.getEndDate());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제의 주요 포인트:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 필터 조건을 별도의 filter() 메소드로 체이닝&lt;/li&gt;
&lt;li&gt;복잡한 조건은 별도 메소드로 분리하여 가독성 향상&lt;/li&gt;
&lt;li&gt;필터 조건을 객체로 캡슐화하여 재사용성 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 중첩된 그룹화 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 비즈니스 요구사항을 처리하기 위한 다중 레벨 그룹화 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class AdvancedGroupingExample {
    public class SalesAnalysis {
        // 지역별, 카테고리별, 월별 매출 분석
        public Map&amp;lt;String, Map&amp;lt;String, Map&amp;lt;YearMonth, SalesStats&amp;gt;&amp;gt;&amp;gt; 
                analyzeRegionalCategorySales(List&amp;lt;Sale&amp;gt; sales) {
            return sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::getRegion,  // 1차: 지역별
                    Collectors.groupingBy(
                        Sale::getCategory,  // 2차: 카테고리별
                        Collectors.groupingBy(
                            sale -&amp;gt; YearMonth.from(sale.getDateTime()),  // 3차: 월별
                            Collectors.collectingAndThen(
                                Collectors.toList(),
                                this::calculateSalesStats
                            )
                        )
                    )
                ));
        }

        private SalesStats calculateSalesStats(List&amp;lt;Sale&amp;gt; sales) {
            // 기본 통계 계산
            DoubleSummaryStatistics stats = sales.stream()
                .mapToDouble(sale -&amp;gt; sale.getAmount().doubleValue())
                .summaryStatistics();

            // 시간대별 분석
            Map&amp;lt;Integer, Long&amp;gt; hourlyDistribution = sales.stream()
                .collect(Collectors.groupingBy(
                    sale -&amp;gt; sale.getDateTime().getHour(),
                    Collectors.counting()
                ));

            // 결제 수단별 분석
            Map&amp;lt;PaymentMethod, BigDecimal&amp;gt; paymentMethodTotal = sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::getPaymentMethod,
                    Collectors.reducing(
                        BigDecimal.ZERO,
                        Sale::getAmount,
                        BigDecimal::add
                    )
                ));

            return new SalesStats(
                BigDecimal.valueOf(stats.getSum()),
                BigDecimal.valueOf(stats.getAverage()),
                sales.size(),
                hourlyDistribution,
                paymentMethodTotal
            );
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 커스텀 Collector 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 집계 요구사항을 처리하기 위한 커스텀 Collector 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;public class MovingAverageCollector&amp;lt;T&amp;gt; implements Collector&amp;lt;T, 
        List&amp;lt;T&amp;gt;, List&amp;lt;Double&amp;gt;&amp;gt; {
    private final int windowSize;
    private final Function&amp;lt;T, Number&amp;gt; valueExtractor;

    public MovingAverageCollector(int windowSize, 
            Function&amp;lt;T, Number&amp;gt; valueExtractor) {
        this.windowSize = windowSize;
        this.valueExtractor = valueExtractor;
    }

    @Override
    public Supplier&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer&amp;lt;List&amp;lt;T&amp;gt;, T&amp;gt; accumulator() {
        return List::add;
    }

    @Override
    public BinaryOperator&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; combiner() {
        return (list1, list2) -&amp;gt; {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Function&amp;lt;List&amp;lt;T&amp;gt;, List&amp;lt;Double&amp;gt;&amp;gt; finisher() {
        return list -&amp;gt; {
            List&amp;lt;Double&amp;gt; result = new ArrayList&amp;lt;&amp;gt;();
            if (list.size() &amp;lt; windowSize) {
                return result;
            }

            for (int i = 0; i &amp;lt;= list.size() - windowSize; i++) {
                double average = list.subList(i, i + windowSize).stream()
                    .mapToDouble(item -&amp;gt; valueExtractor.apply(item).doubleValue())
                    .average()
                    .orElse(0.0);
                result.add(average);
            }
            return result;
        };
    }

    @Override
    public Set&amp;lt;Characteristics&amp;gt; characteristics() {
        return Collections.emptySet();
    }
}

// 사용 예제
public class TimeSeriesAnalyzer {
    public List&amp;lt;Double&amp;gt; calculateMovingAverage(List&amp;lt;Sale&amp;gt; sales, 
            int windowSize) {
        return sales.stream()
            .sorted(Comparator.comparing(Sale::getDateTime))
            .collect(new MovingAverageCollector&amp;lt;&amp;gt;(
                windowSize,
                sale -&amp;gt; sale.getAmount()
            ));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 성능 최적화 기법&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 병렬 스트림 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병렬 스트림을 효과적으로 사용하는 방법과 주의사항을 살펴봅니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class ParallelStreamExample {
    public class PerformanceAnalyzer {
        public Map&amp;lt;String, Object&amp;gt; analyzeLargeDataSet(
                List&amp;lt;Transaction&amp;gt; transactions) {
            int processorCount = Runtime.getRuntime().availableProcessors();

            // 데이터를 청크로 나누어 처리
            int chunkSize = transactions.size() / processorCount;

            return transactions.parallelStream()
                .collect(Collectors.groupingBy(
                    Transaction::getType,
                    Collectors.collectingAndThen(
                        Collectors.toList(),
                        chunk -&amp;gt; {
                            Map&amp;lt;String, Object&amp;gt; results = new HashMap&amp;lt;&amp;gt;();
                            // CPU 집약적 연산 수행
                            results.put(&quot;total&quot;, calculateTotal(chunk));
                            results.put(&quot;risk&quot;, assessRisk(chunk));
                            results.put(&quot;patterns&quot;, findPatterns(chunk));
                            return results;
                        }
                    )
                ));
        }

        // CPU 집약적 연산 예시
        private BigDecimal calculateTotal(List&amp;lt;Transaction&amp;gt; chunk) {
            return chunk.parallelStream()
                .map(Transaction::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 성능 최적화 전략&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.2.1 메모리 효율성 개선&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class MemoryOptimization {
    // 대용량 데이터 처리를 위한 청크 처리
    public void processLargeFile(String filename) {
        try (Stream&amp;lt;String&amp;gt; lines = Files.lines(Paths.get(filename))) {
            lines
                .filter(line -&amp;gt; !line.isEmpty())
                .map(this::parseLine)
                .collect(Collectors.groupingBy(
                    Record::getType,
                    Collectors.mapping(
                        Record::getValue,
                        Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)
                    )
                ));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 스트림 조기 종료를 통한 최적화
    public Optional&amp;lt;Transaction&amp;gt; findFirstLargeTransaction(
            List&amp;lt;Transaction&amp;gt; transactions, BigDecimal threshold) {
        return transactions.stream()
            .filter(t -&amp;gt; t.getAmount().compareTo(threshold) &amp;gt; 0)
            .findFirst();  // 조건을 만족하는 첫 번째 요소를 찾으면 즉시 종료
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 디버깅과 로깅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림 연산의 중간 결과를 확인하고 디버깅하는 방법입니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class StreamDebugging {
    public List&amp;lt;ProcessedData&amp;gt; processDataWithLogging(
            List&amp;lt;RawData&amp;gt; dataList) {
        return dataList.stream()
            .filter(data -&amp;gt; {
                boolean result = isValidData(data);
                log.debug(&quot;Filtering data {}: {}&quot;, data.getId(), result);
                return result;
            })
            .map(data -&amp;gt; {
                ProcessedData result = transformData(data);
                log.debug(&quot;Transformed data {}: {}&quot;, 
                    data.getId(), result);
                return result;
            })
            .peek(data -&amp;gt; {
                if (log.isTraceEnabled()) {
                    log.trace(&quot;Processing data: {}&quot;, data);
                }
            })
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 실전 응용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 실시간 데이터 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 들어오는 데이터를 스트림으로 처리하는 예제입니다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;public class RealTimeProcessor {
    public class TransactionMonitor {
        private Queue&amp;lt;Transaction&amp;gt; recentTransactions = 
            new ConcurrentLinkedQueue&amp;lt;&amp;gt;();

        public void processNewTransaction(Transaction transaction) {
            recentTransactions.add(transaction);

            // 최근 거래 분석
            List&amp;lt;AlertType&amp;gt; alerts = Stream.of(
                    checkAmount(transaction),
                    checkFrequency(transaction),
                    checkPattern(transaction)
                )
                .flatMap(Optional::stream)
                .collect(Collectors.toList());

            if (!alerts.isEmpty()) {
                sendAlerts(alerts);
            }
        }

        private Optional&amp;lt;AlertType&amp;gt; checkFrequency(
                Transaction transaction) {
            long recentCount = recentTransactions.stream()
                .filter(t -&amp;gt; t.getCustomerId()
                    .equals(transaction.getCustomerId()))
                .filter(t -&amp;gt; t.getDateTime()
                    .isAfter(LocalDateTime.now().minusHours(1)))
                .count();

            return recentCount &amp;gt; 10 ? 
                Optional.of(AlertType.HIGH_FREQUENCY) : 
                Optional.empty();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 고급 활용 사례들은 Stream API의 강력한 기능을 실제 비즈니스 요구사항에 적용하는 방법을 보여줍니다. 특히 성능과 메모리 효율성을 고려한 구현은 실제 프로덕션 환경에서 매우 중요합니다.&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/115</guid>
      <comments>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-3-%EA%B3%A0%EA%B8%89-%ED%99%9C%EC%9A%A9%EA%B3%BC-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94#entry115comment</comments>
      <pubDate>Sat, 18 Jan 2025 14:17:06 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Java Stream API 실전 예제 심화</title>
      <link>https://devsite.tistory.com/entry/JAVA-Java-Stream-API-%EC%8B%A4%EC%A0%84-%EC%98%88%EC%A0%9C-%EC%8B%AC%ED%99%94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 복잡한 정렬 시나리오 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 주문 데이터 다중 조건 정렬 예제&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;public class Order {
    private LocalDateTime orderDate;
    private String customerId;
    private BigDecimal amount;
    private OrderStatus status;
    // 생성자, getter, setter 생략
}

// 실제 활용 예시
public class OrderProcessor {
    public List&amp;lt;Order&amp;gt; getProcessedOrders(List&amp;lt;Order&amp;gt; orders) {
        return orders.stream()
            .sorted(
                // 1. 주문 상태 우선순위: PENDING -&amp;gt; PROCESSING -&amp;gt; COMPLETED -&amp;gt; CANCELLED
                Comparator.comparing(Order::getStatus, 
                    Comparator.comparingInt(status -&amp;gt; {
                        switch (status) {
                            case PENDING: return 1;
                            case PROCESSING: return 2;
                            case COMPLETED: return 3;
                            case CANCELLED: return 4;
                            default: return 5;
                        }
                    }))
                // 2. 주문 금액 내림차순
                .thenComparing(Order::getAmount, Comparator.reverseOrder())
                // 3. 주문 일자 오름차순
                .thenComparing(Order::getOrderDate)
            )
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예제에서는:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문 상태별로 우선순위를 부여하여 정렬&lt;/li&gt;
&lt;li&gt;같은 상태 내에서는 주문 금액이 큰 순서대로 정렬&lt;/li&gt;
&lt;li&gt;금액도 같다면 주문 일자 순으로 정렬&lt;/li&gt;
&lt;li&gt;Comparator.comparing()과 thenComparing()을 체이닝하여 복잡한 정렬 조건을 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 사용자 정의 정렬 예제&lt;/h3&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Employee {
    private String name;
    private String department;
    private String position;
    private BigDecimal salary;
    private LocalDate joinDate;
    // 생성자, getter, setter 생략
}

public class HRSystem {
    // 직급별 가중치 정의
    private static Map&amp;lt;String, Integer&amp;gt; positionWeights = new HashMap&amp;lt;&amp;gt;() {{
        put(&quot;사원&quot;, 1);
        put(&quot;대리&quot;, 2);
        put(&quot;과장&quot;, 3);
        put(&quot;차장&quot;, 4);
        put(&quot;부장&quot;, 5);
    }};

    public List&amp;lt;Employee&amp;gt; getSortedEmployees(List&amp;lt;Employee&amp;gt; employees) {
        return employees.stream()
            .sorted(
                // 1. 부서별 그룹핑
                Comparator.comparing(Employee::getDepartment)
                // 2. 직급 가중치 기준 정렬
                .thenComparing(emp -&amp;gt; positionWeights.getOrDefault(emp.getPosition(), 0))
                // 3. 연봉 내림차순
                .thenComparing(Employee::getSalary, Comparator.reverseOrder())
                // 4. 입사일 기준
                .thenComparing(Employee::getJoinDate)
            )
            .collect(Collectors.toList());
    }

    // 부서별 급여 통계 계산
    public Map&amp;lt;String, DepartmentStats&amp;gt; calculateDepartmentStats(List&amp;lt;Employee&amp;gt; employees) {
        return employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    empList -&amp;gt; {
                        DoubleSummaryStatistics salaryStats = empList.stream()
                            .mapToDouble(e -&amp;gt; e.getSalary().doubleValue())
                            .summaryStatistics();

                        return new DepartmentStats(
                            empList.size(),                              // 부서 인원
                            BigDecimal.valueOf(salaryStats.getAverage()),// 평균 급여
                            BigDecimal.valueOf(salaryStats.getMin()),    // 최저 급여
                            BigDecimal.valueOf(salaryStats.getMax()),    // 최고 급여
                            calculateMedianSalary(empList)               // 중간값 급여
                        );
                    }
                )
            ));
    }

    // 중간값 급여 계산
    private BigDecimal calculateMedianSalary(List&amp;lt;Employee&amp;gt; employees) {
        List&amp;lt;BigDecimal&amp;gt; sortedSalaries = employees.stream()
            .map(Employee::getSalary)
            .sorted()
            .collect(Collectors.toList());

        int size = sortedSalaries.size();
        if (size == 0) return BigDecimal.ZERO;

        if (size % 2 == 0) {
            return sortedSalaries.get(size/2 - 1)
                .add(sortedSalaries.get(size/2))
                .divide(new BigDecimal(&quot;2&quot;), 2, RoundingMode.HALF_UP);
        } else {
            return sortedSalaries.get(size/2);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 고급 데이터 변환 예제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 주문 데이터 분석 시스템&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class OrderAnalyzer {
    public class OrderSummary {
        private LocalDate date;
        private Map&amp;lt;String, Integer&amp;gt; productCounts;
        private BigDecimal totalAmount;
        private Set&amp;lt;String&amp;gt; uniqueCustomers;
    }

    public List&amp;lt;OrderSummary&amp;gt; analyzeOrders(List&amp;lt;Order&amp;gt; orders) {
        return orders.stream()
            // 날짜별로 그룹화
            .collect(Collectors.groupingBy(
                order -&amp;gt; order.getOrderDate().toLocalDate(),
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    dailyOrders -&amp;gt; new OrderSummary(
                        dailyOrders.get(0).getOrderDate().toLocalDate(),
                        // 상품별 주문 수량 집계
                        dailyOrders.stream()
                            .flatMap(order -&amp;gt; order.getItems().stream())
                            .collect(Collectors.groupingBy(
                                OrderItem::getProductId,
                                Collectors.summingInt(OrderItem::getQuantity)
                            )),
                        // 총 주문 금액 계산
                        dailyOrders.stream()
                            .map(Order::getAmount)
                            .reduce(BigDecimal.ZERO, BigDecimal::add),
                        // 유니크 고객 수 계산
                        dailyOrders.stream()
                            .map(Order::getCustomerId)
                            .collect(Collectors.toSet())
                    )
                )
            ))
            .values()
            .stream()
            .sorted(Comparator.comparing(OrderSummary::getDate))
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 데이터 변환과 집계의 고급 활용&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class SalesAnalyzer {
    // 지역별, 상품 카테고리별 매출 분석
    public Map&amp;lt;String, Map&amp;lt;String, SalesStatistics&amp;gt;&amp;gt; analyzeSalesByRegionAndCategory(
            List&amp;lt;Sale&amp;gt; sales) {
        return sales.stream()
            .collect(Collectors.groupingBy(
                Sale::getRegion,
                Collectors.groupingBy(
                    Sale::getCategory,
                    Collectors.collectingAndThen(
                        Collectors.toList(),
                        categorySales -&amp;gt; new SalesStatistics(
                            // 총 매출
                            categorySales.stream()
                                .map(Sale::getAmount)
                                .reduce(BigDecimal.ZERO, BigDecimal::add),
                            // 평균 구매 금액
                            categorySales.stream()
                                .map(Sale::getAmount)
                                .reduce(BigDecimal.ZERO, BigDecimal::add)
                                .divide(new BigDecimal(categorySales.size()), 
                                    2, RoundingMode.HALF_UP),
                            // 최대 구매 금액
                            categorySales.stream()
                                .map(Sale::getAmount)
                                .max(Comparator.naturalOrder())
                                .orElse(BigDecimal.ZERO),
                            // 구매 건수
                            categorySales.size(),
                            // 유니크 고객 수
                            categorySales.stream()
                                .map(Sale::getCustomerId)
                                .distinct()
                                .count()
                        )
                    )
                )
            ));
    }

    // 시계열 매출 분석
    public List&amp;lt;TimeSeriesData&amp;gt; analyzeTimeSeriesSales(List&amp;lt;Sale&amp;gt; sales, 
            Period aggregationPeriod) {
        return sales.stream()
            // 기간별로 그룹화
            .collect(Collectors.groupingBy(
                sale -&amp;gt; getAggregationKey(sale.getDateTime(), aggregationPeriod),
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    periodSales -&amp;gt; new TimeSeriesData(
                        // 기간 시작일시
                        getStartOfPeriod(periodSales.get(0).getDateTime(), 
                            aggregationPeriod),
                        // 기간 매출 합계
                        periodSales.stream()
                            .map(Sale::getAmount)
                            .reduce(BigDecimal.ZERO, BigDecimal::add),
                        // 기간 내 평균 주문 금액
                        calculateAverageAmount(periodSales),
                        // 전기 대비 증감률
                        calculateGrowthRate(periodSales)
                    )
                )
            ))
            .values()
            .stream()
            .sorted(Comparator.comparing(TimeSeriesData::getDateTime))
            .collect(Collectors.toList());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 고급 예제들은 실제 업무에서 자주 마주치는 복잡한 데이터 처리 요구사항을 Stream API를 사용하여 효과적으로 해결하는 방법을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 포인트:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;복합 정렬 조건의 구현&lt;/b&gt;: 여러 조건을 조합하여 데이터를 정렬하는 방법&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중첩된 데이터 처리&lt;/b&gt;: 그룹화와 집계를 조합하여 복잡한 통계 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 정의 집계&lt;/b&gt;: 단순 합계나 평균을 넘어선 복잡한 통계 계산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시계열 데이터 처리&lt;/b&gt;: 시간 기반 데이터의 효과적인 처리 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 예제들은 Stream API의 강력한 기능을 실제 비즈니스 로직에 적용하는 방법을 보여주며, 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/114</guid>
      <comments>https://devsite.tistory.com/entry/JAVA-Java-Stream-API-%EC%8B%A4%EC%A0%84-%EC%98%88%EC%A0%9C-%EC%8B%AC%ED%99%94#entry114comment</comments>
      <pubDate>Sat, 18 Jan 2025 14:06:33 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Java Stream API 완벽 가이드 - Part 2: 중급 활용</title>
      <link>https://devsite.tistory.com/entry/JAVA-Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-2-%EC%A4%91%EA%B8%89-%ED%99%9C%EC%9A%A9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 자주 사용되는 연산자 상세 설명&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 sorted() - 정렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;sorted()&lt;/code&gt; 연산자는 스트림의 요소를 정렬할 때 사용합니다. 자연 순서(natural order)나 커스텀 Comparator를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Product {
    private String name;
    private BigDecimal price;
    private String category;
    // 생성자, getter, setter 생략
}

// 기본 정렬 (자연 순서)
List&amp;lt;String&amp;gt; sortedNames = products.stream()
    .map(Product::getName)
    .sorted()
    .collect(Collectors.toList());

// 가격 기준 내림차순 정렬
List&amp;lt;Product&amp;gt; expensiveFirst = products.stream()
    .sorted(Comparator.comparing(Product::getPrice).reversed())
    .collect(Collectors.toList());

// 복합 조건 정렬: 카테고리 오름차순, 같은 카테고리 내에서는 가격 내림차순
List&amp;lt;Product&amp;gt; complexSorted = products.stream()
    .sorted(Comparator
        .comparing(Product::getCategory)
        .thenComparing(Product::getPrice, Comparator.reverseOrder()))
    .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 distinct() - 중복 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;distinct()&lt;/code&gt;는 스트림에서 중복된 요소를 제거합니다. 객체의 경우 equals()와 hashCode() 메서드를 기준으로 판단합니다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 기본 타입의 중복 제거
List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 2, 3, 3, 4);
List&amp;lt;Integer&amp;gt; distinctNumbers = numbers.stream()
    .distinct()
    .collect(Collectors.toList()); // [1, 2, 3, 4]

// 객체의 특정 필드 기준 중복 제거
List&amp;lt;Product&amp;gt; uniqueCategories = products.stream()
    .map(Product::getCategory)
    .distinct()
    .collect(Collectors.toList());

// 복합 키를 기준으로 중복 제거
List&amp;lt;Product&amp;gt; distinctByNameAndCategory = products.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toMap(
            p -&amp;gt; p.getName() + &quot;|&quot; + p.getCategory(), // 복합 키 생성
            Function.identity(),
            (existing, replacement) -&amp;gt; existing // 중복 시 기존 값 유지
        ),
        map -&amp;gt; new ArrayList&amp;lt;&amp;gt;(map.values())
    ));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 limit()와 skip() - 페이징 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 일정 단위로 나누어 처리할 때 유용합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 페이징 처리 유틸리티
public class StreamPaging&amp;lt;T&amp;gt; {
    public List&amp;lt;T&amp;gt; getPage(Stream&amp;lt;T&amp;gt; stream, int pageSize, int pageNumber) {
        return stream
            .skip((long) pageSize * (pageNumber - 1))
            .limit(pageSize)
            .collect(Collectors.toList());
    }
}

// 사용 예제
StreamPaging&amp;lt;Product&amp;gt; paging = new StreamPaging&amp;lt;&amp;gt;();
List&amp;lt;Product&amp;gt; page1 = paging.getPage(products.stream(), 10, 1); // 첫 10개
List&amp;lt;Product&amp;gt; page2 = paging.getPage(products.stream(), 10, 2); // 다음 10개&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.4 flatMap() - 중첩 구조 평탄화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;flatMap()&lt;/code&gt;은 스트림의 각 요소를 다른 스트림으로 변환한 후, 모든 스트림을 하나의 스트림으로 평탄화합니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class Order {
    private List&amp;lt;OrderItem&amp;gt; items;
    // 다른 필드와 메서드 생략
}

public class OrderItem {
    private String productId;
    private int quantity;
    // 다른 필드와 메서드 생략
}

// 모든 주문에서 주문된 상품 ID 목록 추출
List&amp;lt;String&amp;gt; allProductIds = orders.stream()
    .flatMap(order -&amp;gt; order.getItems().stream())
    .map(OrderItem::getProductId)
    .distinct()
    .collect(Collectors.toList());

// 2차원 배열을 1차원으로 평탄화
String[][] arrays = {{&quot;a&quot;, &quot;b&quot;}, {&quot;c&quot;, &quot;d&quot;}, {&quot;e&quot;, &quot;f&quot;}};
List&amp;lt;String&amp;gt; flatList = Arrays.stream(arrays)
    .flatMap(Arrays::stream)
    .collect(Collectors.toList()); // [a, b, c, d, e, f]&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Collectors 클래스 활용&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 기본 수집 연산&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// List로 수집
List&amp;lt;String&amp;gt; nameList = products.stream()
    .map(Product::getName)
    .collect(Collectors.toList());

// Set으로 수집
Set&amp;lt;String&amp;gt; categorySet = products.stream()
    .map(Product::getCategory)
    .collect(Collectors.toSet());

// Map으로 수집
Map&amp;lt;String, Product&amp;gt; productMap = products.stream()
    .collect(Collectors.toMap(
        Product::getName,    // 키 매퍼
        Function.identity(), // 값 매퍼
        (existing, replacement) -&amp;gt; existing // 중복 키 처리
    ));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 그룹화와 분할&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 카테고리별 상품 목록
Map&amp;lt;String, List&amp;lt;Product&amp;gt;&amp;gt; byCategory = products.stream()
    .collect(Collectors.groupingBy(Product::getCategory));

// 카테고리별 평균 가격
Map&amp;lt;String, Double&amp;gt; avgPriceByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.averagingDouble(p -&amp;gt; p.getPrice().doubleValue())
    ));

// 가격대별 상품 수
Map&amp;lt;String, Long&amp;gt; productCountByPriceRange = products.stream()
    .collect(Collectors.groupingBy(
        product -&amp;gt; {
            BigDecimal price = product.getPrice();
            if (price.compareTo(new BigDecimal(&quot;10000&quot;)) &amp;lt; 0) return &quot;저가&quot;;
            if (price.compareTo(new BigDecimal(&quot;50000&quot;)) &amp;lt; 0) return &quot;중가&quot;;
            return &quot;고가&quot;;
        },
        Collectors.counting()
    ));

// 고가/저가 상품 분류
Map&amp;lt;Boolean, List&amp;lt;Product&amp;gt;&amp;gt; partitionedProducts = products.stream()
    .collect(Collectors.partitioningBy(
        p -&amp;gt; p.getPrice().compareTo(new BigDecimal(&quot;50000&quot;)) &amp;gt;= 0
    ));&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 복합 수집 연산&lt;/h3&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// 카테고리별 통계
public class CategoryStats {
    private long count;
    private BigDecimal minPrice;
    private BigDecimal maxPrice;
    private BigDecimal avgPrice;
    // 생성자, getter, setter 생략
}

Map&amp;lt;String, CategoryStats&amp;gt; categoryStats = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            productList -&amp;gt; {
                DoubleSummaryStatistics stats = productList.stream()
                    .mapToDouble(p -&amp;gt; p.getPrice().doubleValue())
                    .summaryStatistics();

                return new CategoryStats(
                    stats.getCount(),
                    BigDecimal.valueOf(stats.getMin()),
                    BigDecimal.valueOf(stats.getMax()),
                    BigDecimal.valueOf(stats.getAverage())
                );
            }
        )
    ));&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Optional과 Stream의 조합&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Optional 스트림 처리&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class Order {
    private String id;
    private Optional&amp;lt;Customer&amp;gt; customer; // 비회원 주문 가능
    // 다른 필드와 메서드 생략
}

// Optional 값이 있는 경우만 처리
List&amp;lt;Customer&amp;gt; validCustomers = orders.stream()
    .map(Order::getCustomer)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList());

// flatMap을 사용한 더 나은 방식
List&amp;lt;Customer&amp;gt; validCustomers = orders.stream()
    .map(Order::getCustomer)
    .flatMap(Optional::stream) // Java 9+
    .collect(Collectors.toList());&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 Optional 활용 예제&lt;/h3&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class OrderProcessor {
    public Optional&amp;lt;OrderSummary&amp;gt; processMemberOrder(Order order) {
        return Optional.of(order)
            .filter(o -&amp;gt; o.getCustomer().isPresent())
            .map(o -&amp;gt; new OrderSummary(
                o.getId(),
                o.getCustomer().get().getName(),
                calculateTotal(o)
            ));
    }

    private BigDecimal calculateTotal(Order order) {
        return order.getItems().stream()
            .map(item -&amp;gt; item.getPrice().multiply(new BigDecimal(item.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스트에서는 Stream API의 중급 활용법에 대해 알아보았습니다. 특히 Collectors의 다양한 활용법과 Optional과의 조합을 통해 더 강력한 데이터 처리가 가능함을 살펴보았습니다. 다음 포스트에서는 Stream API의 고급 활용법과 성능 최적화 방법에 대해 알아보도록 하겠습니다.&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/113</guid>
      <comments>https://devsite.tistory.com/entry/JAVA-Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-2-%EC%A4%91%EA%B8%89-%ED%99%9C%EC%9A%A9#entry113comment</comments>
      <pubDate>Sat, 18 Jan 2025 14:02:12 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] Java Stream API 완벽 가이드 - Part 1: 소개와 기초</title>
      <link>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-1-%EC%86%8C%EA%B0%9C%EC%99%80-%EA%B8%B0%EC%B4%88</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Stream API란?&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 Stream API의 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream API는 Java 8에서 도입된 기능으로, 데이터의 흐름을 추상화하여 컬렉션 데이터를 선언적으로 처리할 수 있게 해주는 API입니다. '흐름'이라는 단어가 의미하듯이, Stream은 데이터 소스로부터 데이터를 읽어서 파이프라인 형태로 처리하는 것을 가능하게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 등장 배경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;함수형 프로그래밍의 필요성&lt;/b&gt;: Java 8에서 람다와 함께 도입되면서 함수형 프로그래밍 패러다임을 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가독성 있는 코드&lt;/b&gt;: 복잡한 데이터 처리를 더 간결하고 이해하기 쉽게 표현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병렬 처리의 용이성&lt;/b&gt;: 멀티코어 환경에서 병렬 처리를 쉽게 구현할 수 있도록 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.3 기존 방식과의 차이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존의 반복문 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;List&amp;lt;Order&amp;gt; orders = getOrders(); // 주문 목록 조회
List&amp;lt;Order&amp;gt; highValueOrders = new ArrayList&amp;lt;&amp;gt;();

// 50만원 이상의 주문을 필터링
for (Order order : orders) {
    if (order.getAmount().compareTo(new BigDecimal(&quot;500000&quot;)) &amp;gt;= 0) {
        highValueOrders.add(order);
    }
}

// 주문 금액 합계 계산
BigDecimal total = BigDecimal.ZERO;
for (Order order : highValueOrders) {
    total = total.add(order.getAmount());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Stream API를 사용한 방식:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;BigDecimal total = orders.stream()
    .filter(order -&amp;gt; order.getAmount().compareTo(new BigDecimal(&quot;500000&quot;)) &amp;gt;= 0)
    .map(Order::getAmount)
    .reduce(BigDecimal.ZERO, BigDecimal::add);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Stream API의 기본 구조&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 스트림 생성 방법&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컬렉션으로부터 생성&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;String&amp;gt; list = Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);
Stream&amp;lt;String&amp;gt; stream = list.stream();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;배열로부터 생성&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;String[] arr = {&quot;a&quot;, &quot;b&quot;, &quot;c&quot;};
Stream&amp;lt;String&amp;gt; stream = Arrays.stream(arr);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;숫자 범위로부터 생성&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;IntStream intStream = IntStream.range(1, 5); // 1,2,3,4
IntStream closedRange = IntStream.rangeClosed(1, 5); // 1,2,3,4,5&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;직접 값을 지정하여 생성&lt;/h4&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Stream&amp;lt;String&amp;gt; stream = Stream.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 중간 연산과 최종 연산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream API의 연산은 중간 연산(Intermediate Operations)과 최종 연산(Terminal Operations)으로 구분됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중간 연산 (Intermediate Operations)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 스트림을 반환&lt;/li&gt;
&lt;li&gt;여러 번 적용 가능&lt;/li&gt;
&lt;li&gt;지연 평가(Lazy Evaluation) 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 중간 연산:&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;// filter: 조건에 맞는 요소 필터링
Stream&amp;lt;T&amp;gt; filter(Predicate&amp;lt;? super T&amp;gt; predicate)

// map: 요소를 변환
&amp;lt;R&amp;gt; Stream&amp;lt;R&amp;gt; map(Function&amp;lt;? super T, ? extends R&amp;gt; mapper)

// sorted: 요소 정렬
Stream&amp;lt;T&amp;gt; sorted()
Stream&amp;lt;T&amp;gt; sorted(Comparator&amp;lt;? super T&amp;gt; comparator)

// distinct: 중복 제거
Stream&amp;lt;T&amp;gt; distinct()

// limit: 요소 개수 제한
Stream&amp;lt;T&amp;gt; limit(long maxSize)

// skip: 처음 n개 요소 제외
Stream&amp;lt;T&amp;gt; skip(long n)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용 예제:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;List&amp;lt;Employee&amp;gt; employees = getEmployees();

List&amp;lt;String&amp;gt; seniorEmployeeNames = employees.stream()
    .filter(emp -&amp;gt; emp.getYearsOfService() &amp;gt; 5)     // 근속년수 5년 초과
    .sorted(Comparator.comparing(Employee::getSalary)) // 급여 순 정렬
    .map(Employee::getName)                         // 이름만 추출
    .distinct()                                     // 중복 제거
    .collect(Collectors.toList());                  // List로 수집&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;최종 연산 (Terminal Operations)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스트림을 소비하고 결과를 반환&lt;/li&gt;
&lt;li&gt;한 번만 적용 가능&lt;/li&gt;
&lt;li&gt;실제 연산 수행을 트리거&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 최종 연산:&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;// forEach: 각 요소에 대해 작업 수행
void forEach(Consumer&amp;lt;? super T&amp;gt; action)

// collect: 결과를 컬렉션으로 수집
&amp;lt;R, A&amp;gt; R collect(Collector&amp;lt;? super T, A, R&amp;gt; collector)

// reduce: 요소들을 하나의 결과로 줄임
Optional&amp;lt;T&amp;gt; reduce(BinaryOperator&amp;lt;T&amp;gt; accumulator)

// count: 요소 개수 반환
long count()

// anyMatch/allMatch/noneMatch: 조건 검사
boolean anyMatch(Predicate&amp;lt;? super T&amp;gt; predicate)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 사용 예제:&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// 부서별 직원 급여 평균 계산
Map&amp;lt;String, Double&amp;gt; avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// 전체 급여 합계 계산
BigDecimal totalSalary = employees.stream()
    .map(Employee::getSalary)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// 고액연봉자 존재 여부 확인
boolean hasHighPaidEmployee = employees.stream()
    .anyMatch(emp -&amp;gt; emp.getSalary().compareTo(new BigDecimal(&quot;100000000&quot;)) &amp;gt; 0);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Stream API의 특징과 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 주요 특징&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;선언형 프로그래밍&lt;/b&gt;: 데이터를 어떻게 처리할지 선언하는 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연 평가&lt;/b&gt;: 최종 연산이 호출될 때까지 중간 연산이 실행되지 않음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재사용 불가&lt;/b&gt;: 스트림은 한 번만 사용 가능&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내부 반복&lt;/b&gt;: 반복 처리를 개발자가 아닌 API가 처리&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 주의사항&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;스트림 재사용 불가&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Stream&amp;lt;String&amp;gt; stream = list.stream();
stream.forEach(System.out::println); // 정상 동작
stream.forEach(System.out::println); // IllegalStateException 발생&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;병렬 스트림 사용 시 주의&lt;/h4&gt;
&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;// 병렬 처리가 항상 더 빠른 것은 아님
List&amp;lt;Integer&amp;gt; numbers = Arrays.asList(1, 2, 3, 4, 5);

// 순차 처리
long sequentialTime = measureTime(() -&amp;gt; 
    numbers.stream().map(this::heavyOperation).count()
);

// 병렬 처리
long parallelTime = measureTime(() -&amp;gt; 
    numbers.parallelStream().map(this::heavyOperation).count()
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.3 실전 활용 예제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;급여 관리 시스템의 예제를 통해 Stream API의 실제 활용을 살펴보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public class SalaryProcessor {
    public Map&amp;lt;String, SalaryStats&amp;gt; analyzeSalaries(List&amp;lt;Employee&amp;gt; employees) {
        return employees.stream()
            // 퇴사자 제외
            .filter(emp -&amp;gt; !emp.isResigned())
            // 부서별 그룹화
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    this::calculateDepartmentStats
                )
            ));
    }

    private SalaryStats calculateDepartmentStats(List&amp;lt;Employee&amp;gt; deptEmployees) {
        DoubleSummaryStatistics stats = deptEmployees.stream()
            .mapToDouble(emp -&amp;gt; emp.getSalary().doubleValue())
            .summaryStatistics();

        return new SalaryStats(
            BigDecimal.valueOf(stats.getAverage()),
            BigDecimal.valueOf(stats.getMin()),
            BigDecimal.valueOf(stats.getMax()),
            stats.getCount()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 Stream API의 기본 개념과 구조, 그리고 주요 사용법에 대해 알아보았습니다. 다음 포스트에서는 Collectors의 고급 활용과 실전에서 자주 사용되는 패턴들에 대해 자세히 다루도록 하겠습니다.&lt;/p&gt;</description>
      <category>언어/JAVA</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/112</guid>
      <comments>https://devsite.tistory.com/entry/Java-Stream-API-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C-Part-1-%EC%86%8C%EA%B0%9C%EC%99%80-%EA%B8%B0%EC%B4%88#entry112comment</comments>
      <pubDate>Sat, 18 Jan 2025 14:00:08 +0900</pubDate>
    </item>
    <item>
      <title>Cursor: AI 시대의 혁신적인 개발 언어 소개</title>
      <link>https://devsite.tistory.com/entry/Cursor-AI-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%98%81%EC%8B%A0%EC%A0%81%EC%9D%B8-%EA%B0%9C%EB%B0%9C-%EC%96%B8%EC%96%B4-%EC%86%8C%EA%B0%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;20240905162755_ckeditor.jpg&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXtfpW/btsKXH2ckuW/U7fk4VpxX70cWoEPc1wf31/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXtfpW/btsKXH2ckuW/U7fk4VpxX70cWoEPc1wf31/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXtfpW/btsKXH2ckuW/U7fk4VpxX70cWoEPc1wf31/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXtfpW%2FbtsKXH2ckuW%2FU7fk4VpxX70cWoEPc1wf31%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;250&quot; data-filename=&quot;20240905162755_ckeditor.jpg&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;들어가며&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 개발 환경이 급속도로 변화하고 있는 가운데, AI 기술을 접목한 새로운 프로그래밍 언어 Cursor가 개발자들의 주목을 받고 있습니다. 이 글에서는 Cursor의 주요 특징과 장점, 그리고 이 혁신적인 도구가 가져올 개발 패러다임의 변화에 대해 자세히 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Cursor란 무엇인가?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor는 인공지능을 기반으로 한 새로운 형태의 프로그래밍 도구입니다. 전통적인 프로그래밍 언어들과는 달리, Cursor는 AI의 강점을 활용하여 개발자의 생산성을 극대화하는 데 초점을 맞추고 있습니다. VSCode를 기반으로 제작되어 익숙한 개발 환경을 제공하면서도, GPT-4와 같은 고급 AI 모델을 통합하여 코드 작성과 디버깅을 획기적으로 개선했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주요 특징&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. AI 기반 코드 자동 완성&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문맥을 이해하는 지능적인 코드 제안&lt;/li&gt;
&lt;li&gt;전체 함수와 클래스 구조를 예측하여 제시&lt;/li&gt;
&lt;li&gt;실시간으로 코드 품질 개선 제안&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 자연어 기반 코드 생성&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;영어나 한국어로 원하는 기능을 설명하면 자동으로 코드 생성&lt;/li&gt;
&lt;li&gt;복잡한 알고리즘이나 디자인 패턴도 쉽게 구현 가능&lt;/li&gt;
&lt;li&gt;생성된 코드에 대한 상세한 설명 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 스마트 디버깅&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 발생 시 원인과 해결책을 자동으로 제시&lt;/li&gt;
&lt;li&gt;코드 최적화 방안 추천&lt;/li&gt;
&lt;li&gt;보안 취약점 자동 감지 및 수정 제안&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;Cursor의 장점&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개발 생산성 향상&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor를 사용하면 반복적인 코드 작성 작업이 크게 줄어듭니다. AI가 보일러플레이트 코드를 자동으로 생성하고, 복잡한 로직도 자연어 설명만으로 구현할 수 있어 개발 시간이 획기적으로 단축됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;학습 곡선 완화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초보 개발자들도 쉽게 접근할 수 있습니다. 자연어로 원하는 기능을 설명하면 AI가 적절한 코드를 제시하고, 그 코드에 대한 상세한 설명도 함께 제공하여 학습 효과도 높습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드 품질 향상&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 지속적으로 코드를 분석하고 개선점을 제시하므로, 더 나은 품질의 코드를 작성할 수 있습니다. 또한 보안 취약점이나 성능 이슈도 사전에 발견하여 수정할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실제 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹 개발&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 사용자 입력: &quot;로그인 폼을 만들어줘&quot;
const LoginForm = () =&amp;gt; {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) =&amp;gt; {
    e.preventDefault();
    // 로그인 로직 구현
  };

  return (
    &amp;lt;form onSubmit={handleSubmit}&amp;gt;
      &amp;lt;input
        type=&quot;email&quot;
        value={email}
        onChange={(e) =&amp;gt; setEmail(e.target.value)}
        placeholder=&quot;이메일&quot;
      /&amp;gt;
      &amp;lt;input
        type=&quot;password&quot;
        value={password}
        onChange={(e) =&amp;gt; setPassword(e.target.value)}
        placeholder=&quot;비밀번호&quot;
      /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 분석&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;# 사용자 입력: &quot;CSV 파일에서 데이터를 읽고 기본적인 통계 분석을 해줘&quot;
import pandas as pd
import numpy as np

def analyze_data(file_path):
    # 데이터 읽기
    df = pd.read_csv(file_path)

    # 기본 통계 계산
    stats = {
        'mean': df.mean(),
        'median': df.median(),
        'std': df.std(),
        'min': df.min(),
        'max': df.max()
    }

    return stats&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;발전 가능성과 전망&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor는 AI 기술의 발전과 함께 계속해서 진화할 것으로 예상됩니다. 특히 다음과 같은 영역에서 더 큰 발전이 기대됩니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;더 정교한 코드 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 비즈니스 로직의 자동 구현&lt;/li&gt;
&lt;li&gt;다양한 프로그래밍 패러다임 지원&lt;/li&gt;
&lt;li&gt;맞춤형 코드 스타일 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;팀 협업 기능 강화&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드 리뷰 자동화&lt;/li&gt;
&lt;li&gt;문서화 자동 생성&lt;/li&gt;
&lt;li&gt;팀 코딩 컨벤션 자동 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;통합 개발 환경으로서의 발전&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더 많은 언어 및 프레임워크 지원&lt;/li&gt;
&lt;li&gt;CI/CD 파이프라인 통합&lt;/li&gt;
&lt;li&gt;클라우드 개발 환경과의 연동&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결론&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor는 AI 시대에 걸맞은 혁신적인 개발 도구입니다. 단순한 코드 에디터를 넘어서서, AI의 능력을 최대한 활용하여 개발자의 생산성을 높이고 코드 품질을 개선하는 데 큰 도움을 줍니다. 특히 자연어 처리 기능을 통해 개발자와 AI 간의 상호작용을 더욱 자연스럽게 만들었다는 점에서 큰 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 Cursor가 더욱 발전하여 개발자들의 필수 도구로 자리잡을 것으로 기대됩니다. AI 기술의 발전과 함께 Cursor도 계속해서 진화할 것이며, 이는 소프트웨어 개발 방식의 큰 변화를 가져올 것입니다. 개발자들은 이러한 변화에 적극적으로 대응하여 새로운 기회를 잡을 필요가 있습니다.&lt;/p&gt;</description>
      <category>언어</category>
      <category>ai개발언어</category>
      <category>cursor</category>
      <category>개발언어</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/111</guid>
      <comments>https://devsite.tistory.com/entry/Cursor-AI-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%98%81%EC%8B%A0%EC%A0%81%EC%9D%B8-%EA%B0%9C%EB%B0%9C-%EC%96%B8%EC%96%B4-%EC%86%8C%EA%B0%9C#entry111comment</comments>
      <pubDate>Wed, 27 Nov 2024 09:20:04 +0900</pubDate>
    </item>
    <item>
      <title>Oracle 힙 구성 테이블(Heap-Organized Tables) 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-%ED%9E%99-%EA%B5%AC%EC%84%B1-%ED%85%8C%EC%9D%B4%EB%B8%94Heap-Organized-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 데이터베이스에서 가장 기본이 되는 테이블 구조인 힙 구성 테이블(Heap-Organized Tables)에 대해 상세히 알아보겠습니다. 실무에서 자주 사용되는 예제와 함께 설명드리겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 힙 구성 테이블이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 구성 테이블은 Oracle의 기본 테이블 구조로, 데이터가 특별한 순서 없이 저장되는 방식입니다. 새로운 데이터는 테이블 세그먼트 내에서 사용 가능한 첫 번째 공간에 저장됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 입력 순서와 무관하게 저장&lt;/li&gt;
&lt;li&gt;ROWID를 통한 데이터 위치 식별&lt;/li&gt;
&lt;li&gt;유연한 저장 공간 관리&lt;/li&gt;
&lt;li&gt;다양한 인덱스 전략 적용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 테이블 생성과 기본 사용법&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 기본 테이블 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 직원 정보 테이블 생성
CREATE TABLE employees (
    employee_id NUMBER PRIMARY KEY,
    first_name VARCHAR2(50),
    last_name VARCHAR2(50),
    email VARCHAR2(100),
    hire_date DATE,
    salary NUMBER(8,2),
    department_id NUMBER
);

-- 부서 정보 테이블 생성
CREATE TABLE departments (
    department_id NUMBER PRIMARY KEY,
    department_name VARCHAR2(100),
    location_id NUMBER
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 데이터 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 데이터 입력
INSERT INTO departments VALUES (10, 'Administration', 1700);
INSERT INTO departments VALUES (20, 'Marketing', 1800);

INSERT INTO employees VALUES (
    1, 'John', 'Smith', 'john.smith@email.com', 
    DATE '2023-01-15', 5000, 10
);

-- 데이터 수정
UPDATE employees
SET salary = salary * 1.1
WHERE department_id = 10;

-- 데이터 삭제
DELETE FROM employees
WHERE hire_date &amp;lt; DATE '2023-01-01';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 인덱스 활용&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 기본 인덱스 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 단일 컬럼 인덱스
CREATE INDEX idx_emp_email 
ON employees(email);

-- 복합 인덱스
CREATE INDEX idx_emp_dept_hire 
ON employees(department_id, hire_date);

-- 함수 기반 인덱스
CREATE INDEX idx_emp_upper_name 
ON employees(UPPER(last_name));&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 인덱스 활용 예제&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;-- 인덱스를 활용한 조회
SELECT e.first_name, e.last_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.hire_date &amp;gt;= DATE '2023-01-01'
AND e.department_id = 10;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 실무 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 주문 관리 시스템&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 주문 테이블
CREATE TABLE orders (
    order_id NUMBER PRIMARY KEY,
    order_date DATE,
    customer_id NUMBER,
    total_amount NUMBER(10,2),
    status VARCHAR2(20)
);

-- 주문 상세 테이블
CREATE TABLE order_items (
    order_id NUMBER,
    product_id NUMBER,
    quantity NUMBER,
    unit_price NUMBER(10,2),
    CONSTRAINT pk_order_items PRIMARY KEY (order_id, product_id)
);

-- 인덱스 생성
CREATE INDEX idx_orders_customer 
ON orders(customer_id);

CREATE INDEX idx_orders_date 
ON orders(order_date);

-- 일별 매출 집계 예제
SELECT 
    TO_CHAR(order_date, 'YYYY-MM-DD') as sale_date,
    COUNT(*) as order_count,
    SUM(total_amount) as total_sales
FROM orders
WHERE order_date &amp;gt;= ADD_MONTHS(SYSDATE, -1)
GROUP BY TO_CHAR(order_date, 'YYYY-MM-DD')
ORDER BY sale_date;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 로그 관리 시스템&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 로그 테이블
CREATE TABLE application_logs (
    log_id NUMBER GENERATED ALWAYS AS IDENTITY,
    log_time TIMESTAMP,
    log_level VARCHAR2(10),
    module VARCHAR2(50),
    message CLOB,
    CONSTRAINT pk_app_logs PRIMARY KEY (log_id)
);

-- 파티션 적용
CREATE TABLE application_logs_part (
    log_id NUMBER GENERATED ALWAYS AS IDENTITY,
    log_time TIMESTAMP,
    log_level VARCHAR2(10),
    module VARCHAR2(50),
    message CLOB,
    CONSTRAINT pk_app_logs_part PRIMARY KEY (log_id)
)
PARTITION BY RANGE (log_time) (
    PARTITION logs_2024_01 VALUES LESS THAN (DATE '2024-02-01'),
    PARTITION logs_2024_02 VALUES LESS THAN (DATE '2024-03-01'),
    PARTITION logs_future VALUES LESS THAN (MAXVALUE)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 테이블 분석&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 테이블 통계 수집
BEGIN
    DBMS_STATS.GATHER_TABLE_STATS(
        ownname =&amp;gt; 'SCOTT',
        tabname =&amp;gt; 'ORDERS',
        estimate_percent =&amp;gt; 100,
        method_opt =&amp;gt; 'FOR ALL COLUMNS SIZE AUTO'
    );
END;
/

-- 테이블 재구성
ALTER TABLE orders MOVE;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 공간 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 테이블 압축
ALTER TABLE orders MOVE COMPRESS FOR ALL OPERATIONS;

-- 공간 회수
ALTER TABLE orders SHRINK SPACE CASCADE;

-- 공간 사용량 확인
SELECT segment_name, 
       bytes/1024/1024 as size_mb,
       blocks,
       extents
FROM user_segments
WHERE segment_type = 'TABLE';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 모니터링 및 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.1 테이블 모니터링&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 테이블 상태 확인
SELECT table_name, 
       num_rows, 
       blocks, 
       empty_blocks,
       avg_row_len
FROM user_tables
WHERE table_name = 'ORDERS';

-- 세그먼트 사용량 확인
SELECT segment_name,
       segment_type,
       bytes/1024/1024 as size_mb,
       blocks,
       extents,
       max_extents
FROM user_segments
WHERE segment_name = 'ORDERS';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 성능 최적화 팁&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;적절한 초기 크기 설정&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1729748724216&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 초기 크기를 지정한 테이블 생성
CREATE TABLE large_table (
id NUMBER,
data VARCHAR2(1000)
)
STORAGE (
INITIAL 64K
NEXT 1M
MAXEXTENTS UNLIMITED
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; &amp;nbsp; 2. 병렬처리활용&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;-- 병렬 처리를 이용한 대량 데이터 처리
ALTER SESSION ENABLE PARALLEL DML;

INSERT /*+ APPEND PARALLEL(orders,4) */ INTO orders
SELECT /*+ PARALLEL(4) */ * FROM orders_staging;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 베스트 프랙티스&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;적절한 데이터 타입 선택&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NUMBER vs VARCHAR2 선택&lt;/li&gt;
&lt;li&gt;DATE vs TIMESTAMP 결정&lt;/li&gt;
&lt;li&gt;CHAR vs VARCHAR2 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 컬럼만 인덱스 생성&lt;/li&gt;
&lt;li&gt;복합 인덱스 순서 최적화&lt;/li&gt;
&lt;li&gt;인덱스 개수 적정화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정기적인 유지보수&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통계 정보 갱신&lt;/li&gt;
&lt;li&gt;테이블 재구성 검토&lt;/li&gt;
&lt;li&gt;불필요한 데이터 아카이빙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 구성 테이블은 Oracle 데이터베이스에서 가장 기본적이면서도 강력한 테이블 구조입니다. 적절한 설계와 관리를 통해 대부분의 업무 요구사항을 효과적으로 처리할 수 있습니다. 특히 일반적인 OLTP 환경에서는 힙 구성 테이블이 최적의 선택이 될 수 있습니다.&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>오라클</category>
      <category>힙테이블</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/110</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-%ED%9E%99-%EA%B5%AC%EC%84%B1-%ED%85%8C%EC%9D%B4%EB%B8%94Heap-Organized-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry110comment</comments>
      <pubDate>Tue, 29 Oct 2024 15:46:49 +0900</pubDate>
    </item>
    <item>
      <title>[Oracle] Oracle 클러스터 테이블(Clustered Tables) 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-Oracle-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%ED%85%8C%EC%9D%B4%EB%B8%94Clustered-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 테이블은 관련된 데이터를 물리적으로 같은 위치에 저장하여 조인 성능을 최적화하는 Oracle의 특별한 저장 구조입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 클러스터 테이블 개요&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관련 데이터를 같은 데이터 블록에 저장&lt;/li&gt;
&lt;li&gt;조인 성능 향상&lt;/li&gt;
&lt;li&gt;디스크 I/O 감소&lt;/li&gt;
&lt;li&gt;저장 공간 효율성 향상&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 클러스터 유형&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;인덱스 클러스터&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클러스터 키에 인덱스 사용&lt;/li&gt;
&lt;li&gt;가장 일반적인 유형&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해시 클러스터&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시 함수로 데이터 위치 결정&lt;/li&gt;
&lt;li&gt;정확한 일치 검색에 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 클러스터 테이블 생성&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 인덱스 클러스터&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 클러스터 생성
CREATE CLUSTER emp_dept_cluster (
    department_id NUMBER(4)
)
SIZE 1024
TABLESPACE users;

-- 클러스터 인덱스 생성
CREATE INDEX idx_emp_dept_cluster 
ON CLUSTER emp_dept_cluster;

-- 클러스터의 첫 번째 테이블 생성
CREATE TABLE departments (
    department_id NUMBER(4) PRIMARY KEY,
    department_name VARCHAR2(30),
    location_id NUMBER(4)
)
CLUSTER emp_dept_cluster (department_id);

-- 클러스터의 두 번째 테이블 생성
CREATE TABLE employees (
    employee_id NUMBER(6) PRIMARY KEY,
    first_name VARCHAR2(20),
    department_id NUMBER(4),
    salary NUMBER(8,2)
)
CLUSTER emp_dept_cluster (department_id);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 해시 클러스터&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 해시 클러스터 생성
CREATE CLUSTER order_cluster (
    order_id NUMBER(10)
)
SIZE 1024
HASHKEYS 10000;

-- 해시 클러스터의 테이블 생성
CREATE TABLE orders (
    order_id NUMBER(10) PRIMARY KEY,
    customer_id NUMBER(10),
    order_date DATE
)
CLUSTER order_cluster (order_id);

CREATE TABLE order_items (
    order_id NUMBER(10),
    line_item_id NUMBER(4),
    product_id NUMBER(10),
    quantity NUMBER(8)
)
CLUSTER order_cluster (order_id);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 부서-직원 데이터 클러스터링&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 클러스터 생성
CREATE CLUSTER hr_cluster (
    dept_id NUMBER(4)
)
SIZE 1024
TABLESPACE users;

CREATE INDEX idx_hr_cluster ON CLUSTER hr_cluster;

-- 부서 테이블
CREATE TABLE dept (
    dept_id NUMBER(4) PRIMARY KEY,
    dept_name VARCHAR2(30)
)
CLUSTER hr_cluster (dept_id);

-- 직원 테이블
CREATE TABLE emp (
    emp_id NUMBER(6) PRIMARY KEY,
    emp_name VARCHAR2(50),
    dept_id NUMBER(4)
)
CLUSTER hr_cluster (dept_id);

-- 클러스터 테이블 조회
SELECT e.emp_name, d.dept_name
FROM emp e, dept d
WHERE e.dept_id = d.dept_id;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 주문-주문상세 클러스터링&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 해시 클러스터 생성
CREATE CLUSTER sales_cluster (
    order_no NUMBER(10)
)
SIZE 1024
HASHKEYS 100000;

-- 주문 테이블
CREATE TABLE orders (
    order_no NUMBER(10) PRIMARY KEY,
    order_date DATE,
    customer_id NUMBER(10)
)
CLUSTER sales_cluster (order_no);

-- 주문상세 테이블
CREATE TABLE order_details (
    order_no NUMBER(10),
    line_no NUMBER(4),
    product_id NUMBER(10),
    quantity NUMBER(8)
)
CLUSTER sales_cluster (order_no);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 클러스터 크기 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 적절한 클러스터 크기 설정
CREATE CLUSTER customer_order_cluster (
    customer_id NUMBER(10)
)
SIZE 8192  -- 8K 블록 크기
TABLESPACE users;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 해시 키 개수 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 예상 데이터량을 고려한 해시 키 설정
CREATE CLUSTER product_cluster (
    product_id NUMBER(10)
)
SIZE 4096
HASHKEYS 50000  -- 예상 제품 수의 1.5배
TABLESPACE users;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 모니터링 및 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 클러스터 정보 조회&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 클러스터 정보 확인
SELECT cluster_name, cluster_type, key_size
FROM USER_CLUSTERS;

-- 클러스터 사용량 확인
SELECT cluster_name, tablespace_name,
       avg_blocks_per_key, avg_data_blocks_per_key
FROM USER_CLUSTERS;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 클러스터 통계 수집&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;-- 클러스터 통계 수집
BEGIN
    DBMS_STATS.GATHER_CLUSTER_STATS(
        ownname =&amp;gt; 'SCOTT',
        clustname =&amp;gt; 'EMP_DEPT_CLUSTER',
        estimate_percent =&amp;gt; 100
    );
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 설계 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.1 클러스터 사용이 적합한 경우&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;자주 조인되는 테이블들&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부서-직원&lt;/li&gt;
&lt;li&gt;주문-주문상세&lt;/li&gt;
&lt;li&gt;고객-주소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터 키로 주로 조회하는 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1:N 관계에서 부모 테이블 기준 조회&lt;/li&gt;
&lt;li&gt;특정 키값으로 관련 데이터를 한꺼번에 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.2 클러스터 사용을 피해야 하는 경우&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터가 자주 변경되는 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클러스터 키가 아닌 컬럼으로 주로 조회하는 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테이블 간 관계가 복잡한 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 성능 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.1 실행 계획 분석&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;-- 클러스터 테이블 조인 실행 계획
EXPLAIN PLAN FOR
SELECT e.emp_name, d.dept_name
FROM emp e, dept d
WHERE e.dept_id = d.dept_id;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.2 클러스터 효율성 분석&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 클러스터 체인 분석
SELECT chain_cnt, avg_chain_len
FROM USER_CLUSTER_HASH_EXPRESSIONS
WHERE cluster_name = 'ORDER_CLUSTER';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 베스트 프랙티스&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;클러스터 키 선택&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적절한 카디널리티&lt;/li&gt;
&lt;li&gt;자주 사용되는 조인 조건&lt;/li&gt;
&lt;li&gt;변경이 적은 컬럼&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;크기 설정&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평균 로우 크기 고려&lt;/li&gt;
&lt;li&gt;예상 성장률 반영&lt;/li&gt;
&lt;li&gt;적절한 버퍼링 팩터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정기적인 통계 수집&lt;/li&gt;
&lt;li&gt;체인 발생 모니터링&lt;/li&gt;
&lt;li&gt;주기적인 재구성 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 테이블은 관련된 데이터를 물리적으로 가깝게 저장함으로써 조인 성능을 크게 향상시킬 수 있는 강력한 기능입니다. 하지만 적절한 사용 케이스 선정과 세심한 설계가 성공적인 구현의 핵심입니다. 특히 데이터의 접근 패턴과 변경 빈도를 충분히 고려하여 클러스터 사용 여부를 결정해야 합니다.&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>오라클</category>
      <category>클러스터</category>
      <category>클러스터테이블</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/109</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-Oracle-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%ED%85%8C%EC%9D%B4%EB%B8%94Clustered-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry109comment</comments>
      <pubDate>Mon, 28 Oct 2024 15:34:10 +0900</pubDate>
    </item>
    <item>
      <title>Oracle 인덱스 구성 테이블(Index-Organized Tables) 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EA%B5%AC%EC%84%B1-%ED%85%8C%EC%9D%B4%EB%B8%94Index-Organized-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 구성 테이블(IOT)은 데이터를 프라이머리 키 순서로 저장하여 조회 성능을 최적화하는 특별한 유형의 테이블입니다. 이 가이드에서는 IOT의 개념부터 실제 활용까지 상세히 다루겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 인덱스 구성 테이블 개요&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터가 프라이머리 키 순서로 저장&lt;/li&gt;
&lt;li&gt;인덱스와 데이터가 하나의 구조로 통합&lt;/li&gt;
&lt;li&gt;추가적인 인덱스 없이 프라이머리 키 기반 조회 최적화&lt;/li&gt;
&lt;li&gt;중복 저장 공간 제거로 스토리지 효율성 향상&lt;/li&gt;
&lt;li&gt;ROWID를 사용하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 일반 테이블과의 차이점&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 일반 테이블의 경우
CREATE TABLE regular_customers (
    customer_id NUMBER PRIMARY KEY,
    name VARCHAR2(100),
    email VARCHAR2(100)
);
-- 데이터는 힙에 저장되고, 별도의 인덱스가 생성됨

-- 인덱스 구성 테이블의 경우
CREATE TABLE iot_customers (
    customer_id NUMBER PRIMARY KEY,
    name VARCHAR2(100),
    email VARCHAR2(100)
) ORGANIZATION INDEX;
-- 데이터가 인덱스 구조 안에 직접 저장됨&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. IOT 생성 및 구성&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 기본 IOT 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 기본적인 IOT 생성
CREATE TABLE orders_iot (
    order_id NUMBER PRIMARY KEY,
    customer_id NUMBER,
    order_date DATE,
    total_amount NUMBER
) ORGANIZATION INDEX;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 오버플로우 세그먼트 활용&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 오버플로우 세그먼트를 사용하는 IOT
CREATE TABLE products_iot (
    product_id NUMBER PRIMARY KEY,
    name VARCHAR2(100),
    description CLOB,
    specifications CLOB
) ORGANIZATION INDEX
  OVERFLOW TABLESPACE products_overflow
  INCLUDING name;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3 보조 인덱스 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- IOT에 보조 인덱스 생성
CREATE INDEX idx_products_name 
ON products_iot(name);

-- 비트맵 인덱스 생성
CREATE BITMAP INDEX idx_products_status
ON products_iot(status);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. IOT 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 조회 위주의 업무&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 우편번호 테이블
CREATE TABLE postal_codes_iot (
    postal_code VARCHAR2(10) PRIMARY KEY,
    city VARCHAR2(100),
    district VARCHAR2(100),
    base_address VARCHAR2(200)
) ORGANIZATION INDEX;

-- 빠른 조회 예제
SELECT base_address
FROM postal_codes_iot
WHERE postal_code = '12345';&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 참조 테이블&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 상품 코드 마스터
CREATE TABLE product_master_iot (
    product_code VARCHAR2(20) PRIMARY KEY,
    category VARCHAR2(50),
    standard_cost NUMBER,
    list_price NUMBER
) ORGANIZATION INDEX
COMPRESS;

-- 관련 테이블에서 참조
CREATE TABLE sales_details (
    sale_id NUMBER,
    product_code VARCHAR2(20) REFERENCES product_master_iot,
    quantity NUMBER,
    unit_price NUMBER
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3 이력 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 고객 주소 이력
CREATE TABLE customer_address_history_iot (
    customer_id NUMBER,
    effective_date DATE,
    address VARCHAR2(200),
    postal_code VARCHAR2(10),
    PRIMARY KEY (customer_id, effective_date)
) ORGANIZATION INDEX;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 키 압축&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 프라이머리 키 압축을 사용한 IOT
CREATE TABLE orders_compressed_iot (
    order_id NUMBER,
    order_date DATE,
    customer_id NUMBER,
    amount NUMBER,
    PRIMARY KEY(order_id, order_date)
) ORGANIZATION INDEX
COMPRESS 1;  -- order_id만 압축&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 병렬 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 병렬 처리를 활용한 데이터 로딩
ALTER SESSION ENABLE PARALLEL DML;

INSERT /*+ APPEND PARALLEL(t,4) */
INTO orders_iot t
SELECT /*+ PARALLEL(s,4) */
    order_id, customer_id, order_date, amount
FROM source_table s;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.3 파티셔닝&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 파티션된 IOT 생성
CREATE TABLE sales_iot (
    sale_date DATE,
    product_id NUMBER,
    quantity NUMBER,
    amount NUMBER,
    PRIMARY KEY (sale_date, product_id)
) ORGANIZATION INDEX
PARTITION BY RANGE (sale_date) (
    PARTITION sales_2023 VALUES LESS THAN (DATE '2024-01-01'),
    PARTITION sales_2024 VALUES LESS THAN (DATE '2025-01-01'),
    PARTITION sales_future VALUES LESS THAN (MAXVALUE)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 관리 및 유지보수&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 통계 수집&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- IOT 통계 수집
ANALYZE TABLE orders_iot COMPUTE STATISTICS
FOR TABLE FOR ALL INDEXES FOR ALL INDEXED COLUMNS;

-- 또는
EXEC DBMS_STATS.GATHER_TABLE_STATS(
    ownname =&amp;gt; 'SCOTT',
    tabname =&amp;gt; 'ORDERS_IOT',
    estimate_percent =&amp;gt; 100,
    method_opt =&amp;gt; 'FOR ALL COLUMNS SIZE AUTO'
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 공간 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- IOT 재구성
ALTER TABLE orders_iot MOVE ONLINE;

-- 오버플로우 세그먼트 재구성
ALTER TABLE orders_iot MOVE OVERFLOW;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.3 모니터링&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- IOT 상태 확인
SELECT table_name, iot_type, iot_name, overflow
FROM USER_TABLES
WHERE iot_type IS NOT NULL;

-- IOT 공간 사용량 확인
SELECT segment_name, segment_type, bytes/1024/1024 as size_mb
FROM USER_SEGMENTS
WHERE segment_name = 'ORDERS_IOT';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 설계 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.1 적합한 사용 케이스&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;프라이머리 키 기반 조회가 많은 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;우편번호 검색&lt;/li&gt;
&lt;li&gt;코드 테이블&lt;/li&gt;
&lt;li&gt;참조 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순차적 접근이 필요한 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이력 데이터&lt;/li&gt;
&lt;li&gt;시계열 데이터&lt;/li&gt;
&lt;li&gt;로그 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장 공간 최적화가 필요한 경우&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 데이터가 많은 테이블&lt;/li&gt;
&lt;li&gt;참조 테이블&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.2 부적합한 사용 케이스&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;대량 DML이 빈번한 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;프라이머리 키가 자주 변경되는 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ROWID를 필요로 하는 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 성능 분석&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.1 실행 계획 분석&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;EXPLAIN PLAN FOR
SELECT * FROM orders_iot
WHERE order_id = 12345;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.2 성능 비교&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 일반 테이블과 IOT 성능 비교
SET TIMING ON

-- IOT 조회
SELECT /*+ INDEX(orders_iot) */ *
FROM orders_iot
WHERE order_id BETWEEN 1000 AND 2000;

-- 일반 테이블 조회
SELECT /*+ INDEX(orders) */ *
FROM orders
WHERE order_id BETWEEN 1000 AND 2000;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 베스트 프랙티스&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;프라이머리 키 설계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자주 접근되는 컬럼 조합&lt;/li&gt;
&lt;li&gt;적절한 카디널리티&lt;/li&gt;
&lt;li&gt;단조 증가하지 않는 값 선호&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오버플로우 세그먼트 활용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 컬럼은 오버플로우로 이동&lt;/li&gt;
&lt;li&gt;INCLUDING 절 적절히 사용&lt;/li&gt;
&lt;li&gt;자주 접근되는 컬럼은 메인 세그먼트에 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;유지보수 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정기적인 통계 수집&lt;/li&gt;
&lt;li&gt;주기적인 재구성 고려&lt;/li&gt;
&lt;li&gt;성능 모니터링 실시&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 구성 테이블(IOT)은 특정 사용 케이스에서 매우 효과적인 성능 최적화 도구입니다. 프라이머리 키 기반의 조회가 많은 업무에서는 탁월한 성능을 제공하며, 저장 공간도 효율적으로 사용할 수 있습니다. 다만, 적절한 사용 케이스 선정과 세심한 설계가 성공적인 구현의 핵심입니다.&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>오라클</category>
      <category>인덱스테이블</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/108</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EA%B5%AC%EC%84%B1-%ED%85%8C%EC%9D%B4%EB%B8%94Index-Organized-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry108comment</comments>
      <pubDate>Sun, 27 Oct 2024 15:16:29 +0900</pubDate>
    </item>
    <item>
      <title>[Oracle] Oracle 파티션 테이블(Partition Tables) 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-%ED%8C%8C%ED%8B%B0%EC%85%98-%ED%85%8C%EC%9D%B4%EB%B8%94Partition-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 테이블은 대용량 데이터를 효율적으로 관리하기 위한 Oracle의 핵심 기능입니다. 이 가이드에서는 파티션 테이블의 개념부터 실제 활용 방법까지 상세히 다루겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 파티션 테이블 개요&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대용량 테이블을 작은 단위로 분할 관리&lt;/li&gt;
&lt;li&gt;데이터 조회/관리 성능 향상&lt;/li&gt;
&lt;li&gt;가용성과 관리 용이성 증가&lt;/li&gt;
&lt;li&gt;독립적인 백업/복구 가능&lt;/li&gt;
&lt;li&gt;파티션 단위의 독립적인 유지보수&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.2 파티션 유형&lt;/b&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Range Partition&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;날짜, 숫자 등 연속된 값 기준&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;List Partition&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불연속적인 값들의 목록 기준&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Hash Partition&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시 알고리즘 기반 균등 분할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Composite Partition&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 가지 이상의 파티션 방식 조합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 파티션 테이블 생성 예제&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 Range Partition&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 연도별 주문 데이터 파티션
CREATE TABLE sales_range (
    sale_id NUMBER,
    sale_date DATE,
    customer_id NUMBER,
    amount NUMBER
)
PARTITION BY RANGE (sale_date) (
    PARTITION sales_2022 VALUES LESS THAN (DATE '2023-01-01')
        TABLESPACE ts_sales_2022,
    PARTITION sales_2023 VALUES LESS THAN (DATE '2024-01-01')
        TABLESPACE ts_sales_2023,
    PARTITION sales_2024 VALUES LESS THAN (DATE '2025-01-01')
        TABLESPACE ts_sales_2024,
    PARTITION sales_future VALUES LESS THAN (MAXVALUE)
        TABLESPACE ts_sales_future
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 List Partition&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 지역별 고객 데이터 파티션
CREATE TABLE customers_list (
    customer_id NUMBER,
    customer_name VARCHAR2(100),
    region VARCHAR2(20),
    credit_limit NUMBER
)
PARTITION BY LIST (region) (
    PARTITION customers_seoul VALUES ('SEOUL'),
    PARTITION customers_busan VALUES ('BUSAN'),
    PARTITION customers_incheon VALUES ('INCHEON'),
    PARTITION customers_others VALUES (DEFAULT)
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3 Hash Partition&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 고객 ID 기반 해시 파티션
CREATE TABLE orders_hash (
    order_id NUMBER,
    customer_id NUMBER,
    order_date DATE,
    amount NUMBER
)
PARTITION BY HASH (customer_id)
PARTITIONS 4
STORE IN (ts_orders1, ts_orders2, ts_orders3, ts_orders4);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.4 Composite Partition&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 연도별-지역별 복합 파티션
CREATE TABLE sales_composite (
    sale_id NUMBER,
    sale_date DATE,
    region VARCHAR2(20),
    amount NUMBER
)
PARTITION BY RANGE (sale_date)
SUBPARTITION BY LIST (region) (
    PARTITION sales_2023 VALUES LESS THAN (DATE '2024-01-01') (
        SUBPARTITION sales_2023_seoul VALUES ('SEOUL'),
        SUBPARTITION sales_2023_busan VALUES ('BUSAN'),
        SUBPARTITION sales_2023_others VALUES (DEFAULT)
    ),
    PARTITION sales_2024 VALUES LESS THAN (DATE '2025-01-01') (
        SUBPARTITION sales_2024_seoul VALUES ('SEOUL'),
        SUBPARTITION sales_2024_busan VALUES ('BUSAN'),
        SUBPARTITION sales_2024_others VALUES (DEFAULT)
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 파티션 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 파티션 추가&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- Range 파티션 추가
ALTER TABLE sales_range 
ADD PARTITION sales_2025 
VALUES LESS THAN (DATE '2026-01-01')
TABLESPACE ts_sales_2025;

-- List 파티션 추가
ALTER TABLE customers_list
ADD PARTITION customers_daegu
VALUES ('DAEGU');&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 파티션 분할&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 기존 파티션을 두 개로 분할
ALTER TABLE sales_range 
SPLIT PARTITION sales_2024 AT (DATE '2024-07-01')
INTO (
    PARTITION sales_2024_h1,
    PARTITION sales_2024_h2
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3 파티션 병합&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 두 파티션을 하나로 병합
ALTER TABLE sales_range
MERGE PARTITIONS sales_2024_h1, sales_2024_h2
INTO PARTITION sales_2024;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.4 파티션 삭제&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 파티션 삭제
ALTER TABLE sales_range
DROP PARTITION sales_2022;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 파티션 관련 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 파티션 프루닝&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 파티션 프루닝을 활용한 조회
SELECT *
FROM sales_range
WHERE sale_date BETWEEN DATE '2023-01-01' AND DATE '2023-12-31';

-- 실행계획 확인
EXPLAIN PLAN FOR
SELECT *
FROM sales_range
WHERE sale_date BETWEEN DATE '2023-01-01' AND DATE '2023-12-31';

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 파티션 인덱스&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 로컬 파티션 인덱스 생성
CREATE INDEX idx_sales_date 
ON sales_range(sale_date) LOCAL;

-- 글로벌 파티션 인덱스 생성
CREATE INDEX idx_sales_customer 
ON sales_range(customer_id) GLOBAL
PARTITION BY HASH (customer_id)
PARTITIONS 4;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 실제 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 이력 데이터 관리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 거래 이력 테이블
CREATE TABLE transaction_history (
    trans_id NUMBER,
    trans_date DATE,
    account_id NUMBER,
    amount NUMBER,
    trans_type VARCHAR2(10)
)
PARTITION BY RANGE (trans_date)
INTERVAL(NUMTOYMINTERVAL(1, 'MONTH')) (
    PARTITION trans_first 
    VALUES LESS THAN (DATE '2023-01-01')
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 데이터 아카이빙&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 오래된 데이터 아카이브
CREATE TABLE sales_archive
TABLESPACE arch_ts
AS
SELECT * FROM sales_range
WHERE sale_date &amp;lt; DATE '2023-01-01';

ALTER TABLE sales_range
DROP PARTITION sales_2022;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 파티션 관련 모니터링&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.1 파티션 정보 조회&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 파티션 정보 조회
SELECT table_name, partition_name, high_value
FROM ALL_TAB_PARTITIONS
WHERE table_name = 'SALES_RANGE';

-- 파티션별 통계 정보
SELECT partition_name, num_rows, blocks
FROM ALL_TAB_PARTITIONS
WHERE table_name = 'SALES_RANGE';&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.2 파티션 사용량 모니터링&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;SELECT p.partition_name,
       p.tablespace_name,
       p.num_rows,
       p.blocks * t.block_size / 1024 / 1024 as size_mb
FROM all_tab_partitions p,
     dba_tablespaces t
WHERE p.table_name = 'SALES_RANGE'
AND p.tablespace_name = t.tablespace_name;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 파티션 테이블 관리 팁&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;파티션 키 선택&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;조회 패턴 분석&lt;/li&gt;
&lt;li&gt;데이터 분포 고려&lt;/li&gt;
&lt;li&gt;파티션 프루닝 효과 최대화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파티션 크기&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;균등한 크기 유지&lt;/li&gt;
&lt;li&gt;관리 용이성 고려&lt;/li&gt;
&lt;li&gt;성능과 관리의 균형&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백업 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션 단위 백업&lt;/li&gt;
&lt;li&gt;점진적 백업 구현&lt;/li&gt;
&lt;li&gt;복구 시간 최소화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 성능 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;파티션 개수&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;너무 많은 파티션은 관리 복잡도 증가&lt;/li&gt;
&lt;li&gt;너무 적은 파티션은 성능 이점 감소&lt;/li&gt;
&lt;li&gt;적절한 균형점 찾기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인덱스 전략&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 vs 글로벌 인덱스 선택&lt;/li&gt;
&lt;li&gt;파티션 키 포함 여부&lt;/li&gt;
&lt;li&gt;유지보수 영향도 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병렬 처리&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1729743181024&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- 병렬 처리를 활용한 대량 데이터 처리

ALTER SESSION ENABLE PARALLEL DML;

INSERT /+ APPEND PARALLEL(s,4) */
INTO sales_range
SELECT /+ PARALLEL(4) */ *
FROM source_table&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 테이블은 대용량 데이터베이스 관리의 핵심 기능입니다. 적절한 파티션 전략 수립과 구현을 통해 성능, 가용성, 관리 효율성을 크게 향상시킬 수 있습니다. 다만, 업무 특성과 데이터 특성을 충분히 고려한 설계가 필요하며, 지속적인 모니터링과 관리가 필요합니다.&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>오라클</category>
      <category>파티션</category>
      <category>파티션테이블</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/107</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-%ED%8C%8C%ED%8B%B0%EC%85%98-%ED%85%8C%EC%9D%B4%EB%B8%94Partition-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry107comment</comments>
      <pubDate>Sat, 26 Oct 2024 14:15:08 +0900</pubDate>
    </item>
    <item>
      <title>[Oracle ] Oracle 외부 테이블(External Tables) 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-Oracle-%EC%99%B8%EB%B6%80-%ED%85%8C%EC%9D%B4%EB%B8%94External-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O770E/btsKh1M048Q/Seq3uXVvFkO10M7tkYdCH1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O770E/btsKh1M048Q/Seq3uXVvFkO10M7tkYdCH1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O770E/btsKh1M048Q/Seq3uXVvFkO10M7tkYdCH1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO770E%2FbtsKh1M048Q%2FSeq3uXVvFkO10M7tkYdCH1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;125&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 테이블은 Oracle 데이터베이스에서 외부 파일의 데이터를 마치 데이터베이스 테이블처럼 조회할 수 있게 해주는 강력한 기능입니다. ETL 작업이나 대용량 데이터 로딩에 특히 유용합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 외부 테이블의 특징&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 파일을 데이터베이스 테이블처럼 조회 가능&lt;/li&gt;
&lt;li&gt;기본적으로 읽기 전용&lt;/li&gt;
&lt;li&gt;SQL 문을 통한 데이터 조회 가능&lt;/li&gt;
&lt;li&gt;일반 테이블과의 조인 가능&lt;/li&gt;
&lt;li&gt;병렬 처리 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2 장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 웨어하우스 로딩 작업 단순화&lt;/li&gt;
&lt;li&gt;이기종 데이터 소스 통합&lt;/li&gt;
&lt;li&gt;스키마 온 리드(Schema-on-Read) 구현&lt;/li&gt;
&lt;li&gt;ETL 프로세스 최적화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 외부 테이블 생성 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 디렉토리 객체 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 관리자 권한 필요
CREATE DIRECTORY ext_data_dir AS '/oracle/external/data';
GRANT READ, WRITE ON DIRECTORY ext_data_dir TO scott;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 기본 외부 테이블 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE emp_ext (
    emp_id NUMBER,
    emp_name VARCHAR2(100),
    salary NUMBER,
    hire_date DATE
)
ORGANIZATION EXTERNAL (
    TYPE ORACLE_LOADER
    DEFAULT DIRECTORY ext_data_dir
    ACCESS PARAMETERS (
        RECORDS DELIMITED BY NEWLINE
        FIELDS TERMINATED BY ','
        MISSING FIELD VALUES ARE NULL
        (
            emp_id CHAR(5),
            emp_name CHAR(100),
            salary CHAR(10),
            hire_date DATE &quot;YYYY-MM-DD&quot;
        )
    )
    LOCATION ('employees.csv')
)
REJECT LIMIT UNLIMITED;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3 ORACLE_DATAPUMP 사용 예제&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE TABLE sales_ext
ORGANIZATION EXTERNAL (
    TYPE ORACLE_DATAPUMP
    DEFAULT DIRECTORY ext_data_dir
    LOCATION ('sales.dmp')
)
AS SELECT * FROM sales WHERE year = 2023;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 고급 활용 예제&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 다중 파일 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE sales_multi_ext (
    sale_date DATE,
    product_id NUMBER,
    amount NUMBER
)
ORGANIZATION EXTERNAL (
    TYPE ORACLE_LOADER
    DEFAULT DIRECTORY ext_data_dir
    ACCESS PARAMETERS (
        RECORDS DELIMITED BY NEWLINE
        FIELDS TERMINATED BY ','
        (
            sale_date DATE &quot;YYYY-MM-DD&quot;,
            product_id INTEGER EXTERNAL,
            amount INTEGER EXTERNAL
        )
    )
    LOCATION (
        'sales_2023.csv',
        'sales_2024.csv'
    )
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 병렬 처리 활용&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 병렬 처리를 사용한 데이터 로딩
INSERT /*+ APPEND PARALLEL(sales_target, 4) */ 
INTO sales_target
SELECT /*+ PARALLEL(s, 4) */
    sale_date,
    product_id,
    amount
FROM sales_ext s;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3 데이터 변환과 필터링&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE transformed_ext (
    order_id NUMBER,
    order_date DATE,
    total_amount NUMBER
)
ORGANIZATION EXTERNAL (
    TYPE ORACLE_LOADER
    DEFAULT DIRECTORY ext_data_dir
    ACCESS PARAMETERS (
        RECORDS DELIMITED BY NEWLINE
        FIELDS TERMINATED BY ','
        (
            order_id CHAR(10),
            order_date CHAR(10) DATE_FORMAT DATE MASK &quot;YYYY-MM-DD&quot;,
            total_amount CHAR(15) FLOAT EXTERNAL
        )
    )
    LOCATION ('orders.csv')
)
REJECT LIMIT UNLIMITED;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 실제 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 ETL 프로세스&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 스테이징 테이블로 데이터 로드
CREATE TABLE stage_sales AS
SELECT * 
FROM sales_ext
WHERE sale_date &amp;gt;= DATE '2024-01-01';

-- 데이터 변환 및 정제
INSERT INTO sales_dwh
SELECT 
    TO_CHAR(sale_date, 'YYYYMM') as sale_month,
    product_id,
    SUM(amount) as total_amount,
    COUNT(*) as transaction_count
FROM stage_sales
GROUP BY 
    TO_CHAR(sale_date, 'YYYYMM'),
    product_id;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 로그 파일 분석&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE log_analysis_ext (
    log_time TIMESTAMP,
    level VARCHAR2(10),
    message CLOB
)
ORGANIZATION EXTERNAL (
    TYPE ORACLE_LOADER
    DEFAULT DIRECTORY log_dir
    ACCESS PARAMETERS (
        RECORDS DELIMITED BY NEWLINE
        FIELDS TERMINATED BY '|'
        (
            log_time CHAR(23) DATE_FORMAT TIMESTAMP MASK &quot;YYYY-MM-DD HH24:MI:SS.FF&quot;,
            level CHAR(10),
            message CHAR(4000)
        )
    )
    LOCATION ('app_log.txt')
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 성능 최적화&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.1 파티셔닝 활용&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE sales_ext_part (
    sale_date DATE,
    product_id NUMBER,
    amount NUMBER
)
ORGANIZATION EXTERNAL (
    TYPE ORACLE_LOADER
    DEFAULT DIRECTORY ext_data_dir
    ACCESS PARAMETERS (
        RECORDS DELIMITED BY NEWLINE
        FIELDS TERMINATED BY ','
    )
    LOCATION (
        'sales_2023Q1.csv',
        'sales_2023Q2.csv'
    )
)
PARTITION BY RANGE (sale_date)
(
    PARTITION sales_q1_2023 VALUES LESS THAN (DATE '2023-04-01'),
    PARTITION sales_q2_2023 VALUES LESS THAN (DATE '2023-07-01')
);&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.2 인덱스 활용&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 외부 테이블 데이터를 기반으로 한 구체화된 뷰 생성
CREATE MATERIALIZED VIEW mv_sales_summary
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND
AS
SELECT 
    TO_CHAR(sale_date, 'YYYYMM') as sale_month,
    SUM(amount) as total_sales
FROM sales_ext
GROUP BY TO_CHAR(sale_date, 'YYYYMM');

-- 구체화된 뷰에 인덱스 생성
CREATE INDEX idx_mv_sales_summary ON mv_sales_summary(sale_month);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 오류 처리 및 로깅&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.1 리젝트 파일 설정&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE orders_ext (
    order_id NUMBER,
    order_date DATE,
    amount NUMBER
)
ORGANIZATION EXTERNAL (
    TYPE ORACLE_LOADER
    DEFAULT DIRECTORY ext_data_dir
    ACCESS PARAMETERS (
        RECORDS DELIMITED BY NEWLINE
        BADFILE 'orders.bad'
        LOGFILE 'orders.log'
        FIELDS TERMINATED BY ','
        MISSING FIELD VALUES ARE NULL
    )
    LOCATION ('orders.csv')
)
REJECT LIMIT 100;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;6.2 오류 확인&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 외부 테이블 처리 중 발생한 오류 확인
SELECT *
FROM ALL_EXTERNAL_TABLES
WHERE TABLE_NAME = 'ORDERS_EXT';

SELECT *
FROM ALL_EXTERNAL_LOCATIONS
WHERE TABLE_NAME = 'ORDERS_EXT';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 모니터링 및 관리&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;7.1 상태 확인&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;-- 외부 테이블 상태 확인
SELECT owner, table_name, type_name, default_directory_name
FROM ALL_EXTERNAL_TABLES;

-- 외부 테이블 위치 정보
SELECT * FROM ALL_EXTERNAL_LOCATIONS;

-- 외부 테이블 컬럼 정보
SELECT column_name, data_type, data_length
FROM ALL_TAB_COLUMNS
WHERE TABLE_NAME = 'SALES_EXT';&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;8. 보안 고려사항&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;디렉토리 객체 권한&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요한 최소 권한만 부여&lt;/li&gt;
&lt;li&gt;정기적인 권한 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파일 시스템 보안&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 파일에 대한 적절한 파일 시스템 권한 설정&lt;/li&gt;
&lt;li&gt;중요 데이터 암호화 고려&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 테이블은 Oracle 데이터베이스에서 외부 데이터를 효율적으로 처리할 수 있는 강력한 기능입니다. ETL 프로세스, 로그 분석, 데이터 마이그레이션 등 다양한 용도로 활용할 수 있으며, 적절한 설정과 관리를 통해 높은 성능과 안정성을 확보할 수 있습니다.&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>오라클</category>
      <category>외부테이블</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/106</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-Oracle-%EC%99%B8%EB%B6%80-%ED%85%8C%EC%9D%B4%EB%B8%94External-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry106comment</comments>
      <pubDate>Fri, 25 Oct 2024 10:36:05 +0900</pubDate>
    </item>
    <item>
      <title>[Oracle] Oracle 임시 테이블(Temporary Tables) 완벽 가이드</title>
      <link>https://devsite.tistory.com/entry/Oracle-%EC%9E%84%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94Temporary-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s4YjG/btsKg9LU8Z0/hEdMBUVOgseIoLTA9r60SK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s4YjG/btsKg9LU8Z0/hEdMBUVOgseIoLTA9r60SK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s4YjG/btsKg9LU8Z0/hEdMBUVOgseIoLTA9r60SK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs4YjG%2FbtsKg9LU8Z0%2FhEdMBUVOgseIoLTA9r60SK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;402&quot; height=&quot;125&quot; data-origin-width=&quot;402&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Oracle 데이터베이스에서 임시 테이블은 일시적인 데이터를 저장하고 관리하는데 매우 유용한 객체입니다. 이 글에서는 임시 테이블의 특징, 생성 방법, 활용 사례 등을 자세히 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 임시 테이블이란?&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 테이블은 세션 또는 트랜잭션 수준에서 데이터를 유지하는 특수한 형태의 테이블입니다. 테이블 정의는 모든 세션이 공유하지만, 데이터는 각 세션별로 독립적으로 관리됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1 주요 특징&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션별 독립적인 데이터 관리&lt;/li&gt;
&lt;li&gt;세션 종료 시 자동 데이터 삭제&lt;/li&gt;
&lt;li&gt;다른 세션의 데이터는 볼 수 없음&lt;/li&gt;
&lt;li&gt;일반 테이블과 동일한 인덱스, 제약조건 사용 가능&lt;/li&gt;
&lt;li&gt;TRUNCATE, DELETE 등의 DDL/DML 명령어 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 임시 테이블 생성 방법&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1 기본 문법&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE GLOBAL TEMPORARY TABLE 테이블명 (
    컬럼정의
) ON COMMIT [DELETE ROWS | PRESERVE ROWS];&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2 ON COMMIT 옵션&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DELETE ROWS&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 종료될 때 데이터 삭제&lt;/li&gt;
&lt;li&gt;짧은 트랜잭션에서 임시 데이터 처리할 때 유용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PRESERVE ROWS&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션이 종료될 때까지 데이터 유지&lt;/li&gt;
&lt;li&gt;세션 내에서 여러 트랜잭션에 걸쳐 데이터를 유지해야 할 때 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 실전 예제&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1 기본 임시 테이블 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 트랜잭션 단위로 데이터가 삭제되는 임시 테이블
CREATE GLOBAL TEMPORARY TABLE temp_orders (
    order_id NUMBER,
    customer_id NUMBER,
    order_date DATE,
    total_amount NUMBER
) ON COMMIT DELETE ROWS;

-- 세션 종료시 데이터가 삭제되는 임시 테이블
CREATE GLOBAL TEMPORARY TABLE temp_order_items (
    item_id NUMBER,
    order_id NUMBER,
    product_id NUMBER,
    quantity NUMBER,
    unit_price NUMBER
) ON COMMIT PRESERVE ROWS;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2 활용 예제: 대량 데이터 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 임시 테이블을 사용한 대량 주문 처리
DECLARE
    v_batch_size NUMBER := 1000;
BEGIN
    -- 임시 테이블에 처리할 주문 데이터 저장
    INSERT INTO temp_orders
    SELECT order_id, customer_id, order_date, total_amount
    FROM orders
    WHERE process_flag = 'N'
    AND ROWNUM &amp;lt;= v_batch_size;

    -- 임시 테이블의 데이터 처리
    UPDATE orders o
    SET o.process_flag = 'Y'
    WHERE EXISTS (
        SELECT 1 
        FROM temp_orders t 
        WHERE t.order_id = o.order_id
    );

    COMMIT;
END;
/&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3 인덱스 생성&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 임시 테이블에 인덱스 생성
CREATE INDEX idx_temp_orders_01 
ON temp_orders (order_date, customer_id);

-- 임시 테이블에 기본키 제약조건 추가
ALTER TABLE temp_orders 
ADD CONSTRAINT pk_temp_orders PRIMARY KEY (order_id);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 활용 사례&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.1 복잡한 계산 결과 저장&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대량 데이터의 중간 집계 결과 저장&lt;/li&gt;
&lt;li&gt;다단계 처리가 필요한 배치 작업의 중간 결과 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.2 데이터 정제 작업&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ETL 과정에서 데이터 검증&lt;/li&gt;
&lt;li&gt;중복 데이터 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.3 성능 최적화&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복잡한 조인 쿼리의 중간 결과 저장&lt;/li&gt;
&lt;li&gt;반복적으로 사용되는 데이터 캐싱&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 주의사항&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리 사용&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;임시 테이블의 데이터는 TEMP 테이블스페이스를 사용&lt;/li&gt;
&lt;li&gt;대량의 데이터 처리 시 TEMP 테이블스페이스 크기 고려 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ON COMMIT DELETE ROWS 사용 시 COMMIT 시점 관리 중요&lt;/li&gt;
&lt;li&gt;불필요한 데이터는 즉시 삭제하여 리소스 확보&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세션 종료 전 중요 데이터 백업 필요&lt;/li&gt;
&lt;li&gt;장시간 실행되는 세션의 경우 리소스 사용량 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;6. 성능 최적화 팁&lt;/b&gt;&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 병렬 처리를 위한 힌트 사용
INSERT /*+ APPEND PARALLEL(t,4) */ INTO temp_orders t
SELECT /*+ PARALLEL(o,4) */ *
FROM orders o
WHERE process_flag = 'N';

-- 효율적인 조인을 위한 임시 테이블 활용
CREATE GLOBAL TEMPORARY TABLE temp_summary
ON COMMIT PRESERVE ROWS
AS
SELECT /*+ PARALLEL(4) */
    customer_id,
    COUNT(*) as order_count,
    SUM(total_amount) as total_sales
FROM orders
GROUP BY customer_id;

-- 인덱스를 활용한 조회 성능 향상
CREATE INDEX idx_temp_summary
ON temp_summary(customer_id);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;7. 모니터링 및 관리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 테이블의 효율적인 관리를 위해 다음 뷰들을 활용할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- 임시 테이블 사용량 모니터링
SELECT *
FROM v$temp_space_header;

-- 세션별 임시 테이블 사용량
SELECT s.sid, s.serial#, s.username,
       t.blocks * TBS.block_size / 1024 / 1024 as &quot;Size (MB)&quot;
FROM v$session s,
     v$tempseg_usage t,
     dba_tablespaces TBS
WHERE s.saddr = t.session_addr
AND t.tablespace = TBS.tablespace_name;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임시 테이블은 Oracle 데이터베이스에서 세션 기반의 데이터 처리를 위한 강력한 도구입니다. 적절히 활용하면 복잡한 데이터 처리 작업을 효율적으로 수행할 수 있으며, 전체 애플리케이션의 성능을 향상시킬 수 있습니다. 다만, 리소스 사용량과 세션 관리에 주의를 기울여야 하며, 적절한 모니터링과 관리가 필요합니다.&lt;/p&gt;</description>
      <category>DB/Oracle</category>
      <category>오라클</category>
      <category>임시테이블</category>
      <category>활용방법</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/105</guid>
      <comments>https://devsite.tistory.com/entry/Oracle-%EC%9E%84%EC%8B%9C-%ED%85%8C%EC%9D%B4%EB%B8%94Temporary-Tables-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C#entry105comment</comments>
      <pubDate>Thu, 24 Oct 2024 11:18:03 +0900</pubDate>
    </item>
    <item>
      <title>[React] React로 마트 계산기 만들기</title>
      <link>https://devsite.tistory.com/entry/React%EB%A1%9C-%EB%A7%88%ED%8A%B8-%EA%B3%84%EC%82%B0%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dla5H5/btsKfw1wndN/LdtrQxurPdxSdzPnG4AZfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dla5H5/btsKfw1wndN/LdtrQxurPdxSdzPnG4AZfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dla5H5/btsKfw1wndN/LdtrQxurPdxSdzPnG4AZfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdla5H5%2FbtsKfw1wndN%2FLdtrQxurPdxSdzPnG4AZfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;472&quot; height=&quot;229&quot; data-origin-width=&quot;766&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 React와 Tailwind CSS를 사용하여 실용적인 마트 계산기를 만드는 방법을 알아보겠습니다. 이 프로젝트를 통해 실제 상황에서 사용할 수 있는 계산기를 구현하면서 React의 상태 관리와 UI 구성 방법을 배워볼 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주요 기능&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 정보 입력 (상품명, 가격, 수량, 할인율)&lt;/li&gt;
&lt;li&gt;상품 목록 관리 (추가/삭제)&lt;/li&gt;
&lt;li&gt;할인율 적용 및 계산&lt;/li&gt;
&lt;li&gt;총액 자동 계산&lt;/li&gt;
&lt;li&gt;한국 원화 형식으로 금액 표시&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용된 기술&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React.js&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;shadcn/ui (Card, Button, Input 컴포넌트)&lt;/li&gt;
&lt;li&gt;Lucide React (아이콘)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기능 구현&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 상태 관리 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 필요한 상태들을 정의합니다:&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [items, setItems] = useState([]); // 상품 목록
const [currentItem, setCurrentItem] = useState({
  name: '',
  price: '',
  quantity: '1',
  discount: '0'
}); // 현재 입력 중인 상품
const [total, setTotal] = useState(0); // 총액&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 상품 추가 기능&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const addItem = () =&amp;gt; {
  if (currentItem.name &amp;amp;&amp;amp; currentItem.price) {
    const price = parseFloat(currentItem.price);
    const quantity = parseInt(currentItem.quantity) || 1;
    const discount = parseFloat(currentItem.discount) || 0;

    const subtotal = (price * quantity) * (1 - discount / 100);

    const newItem = {
      ...currentItem,
      price: parseFloat(currentItem.price),
      quantity: parseInt(currentItem.quantity),
      discount: parseFloat(currentItem.discount),
      subtotal: subtotal
    };

    setItems([...items, newItem]);
    setCurrentItem({
      name: '',
      price: '',
      quantity: '1',
      discount: '0'
    });

    calculateTotal([...items, newItem]);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 금액 계산 및 포맷팅&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const calculateTotal = (currentItems) =&amp;gt; {
  const newTotal = currentItems.reduce((sum, item) =&amp;gt; sum + item.subtotal, 0);
  setTotal(newTotal);
};

const formatCurrency = (amount) =&amp;gt; {
  return new Intl.NumberFormat('ko-KR', {
    style: 'currency',
    currency: 'KRW'
  }).format(amount);
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;UI 구현&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 입력 폼 구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 정보를 입력받는 폼을 그리드 레이아웃으로 구성합니다:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;grid grid-cols-12 gap-2 mb-4&quot;&amp;gt;
  &amp;lt;Input
    className=&quot;col-span-3&quot;
    placeholder=&quot;상품명&quot;
    value={currentItem.name}
    onChange={(e) =&amp;gt; setCurrentItem({...currentItem, name: e.target.value})}
  /&amp;gt;
  &amp;lt;Input
    className=&quot;col-span-3&quot;
    placeholder=&quot;가격&quot;
    type=&quot;number&quot;
    value={currentItem.price}
    onChange={(e) =&amp;gt; setCurrentItem({...currentItem, price: e.target.value})}
  /&amp;gt;
  {/* 수량과 할인율 입력 필드 */}
  &amp;lt;Button className=&quot;col-span-2&quot; onClick={addItem}&amp;gt;
    &amp;lt;Plus className=&quot;mr-1 h-4 w-4&quot; /&amp;gt; 추가
  &amp;lt;/Button&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 상품 목록 테이블&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML table 요소를 사용하여 상품 목록을 표시합니다:&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;table className=&quot;w-full&quot;&amp;gt;
  &amp;lt;thead className=&quot;bg-gray-50&quot;&amp;gt;
    &amp;lt;tr&amp;gt;
      &amp;lt;th className=&quot;py-2 px-4 text-left&quot;&amp;gt;상품명&amp;lt;/th&amp;gt;
      &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;가격&amp;lt;/th&amp;gt;
      &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;수량&amp;lt;/th&amp;gt;
      &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;할인&amp;lt;/th&amp;gt;
      &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;소계&amp;lt;/th&amp;gt;
      &amp;lt;th className=&quot;py-2 px-4 w-[50px]&quot;&amp;gt;&amp;lt;/th&amp;gt;
    &amp;lt;/tr&amp;gt;
  &amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    {items.map((item, index) =&amp;gt; (
      &amp;lt;tr key={index} className=&quot;border-t&quot;&amp;gt;
        {/* 상품 정보 표시 */}
      &amp;lt;/tr&amp;gt;
    ))}
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 총액 표시&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산된 총액을 시각적으로 강조하여 표시합니다:&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;div className=&quot;mt-4 flex justify-end&quot;&amp;gt;
  &amp;lt;div className=&quot;bg-blue-50 p-4 rounded-lg&quot;&amp;gt;
    &amp;lt;span className=&quot;text-lg font-semibold mr-2&quot;&amp;gt;총액:&amp;lt;/span&amp;gt;
    &amp;lt;span className=&quot;text-2xl font-bold text-blue-600&quot;&amp;gt;
      {formatCurrency(total)}
    &amp;lt;/span&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주요 기능 설명&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 상품 입력 처리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 입력한 상품 정보를 실시간으로 상태에 반영&lt;/li&gt;
&lt;li&gt;필수 필드(상품명, 가격) 검증&lt;/li&gt;
&lt;li&gt;수량과 할인율의 기본값 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 계산 로직&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개별 상품의 소계 = 가격 &amp;times; 수량 &amp;times; (1 - 할인율/100)&lt;/li&gt;
&lt;li&gt;총액 = 모든 상품의 소계 합계&lt;/li&gt;
&lt;li&gt;할인율은 퍼센트 단위로 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 데이터 관리&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 목록은 배열로 관리&lt;/li&gt;
&lt;li&gt;각 상품은 고유 인덱스로 식별&lt;/li&gt;
&lt;li&gt;상품 삭제 시 자동으로 총액 재계산&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;개선 가능한 부분&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 마트 계산기는 다음과 같은 방향으로 확장할 수 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;기능 확장&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 코드/바코드 지원&lt;/li&gt;
&lt;li&gt;자주 사용하는 상품 즐겨찾기&lt;/li&gt;
&lt;li&gt;결제 수단 선택&lt;/li&gt;
&lt;li&gt;영수증 출력&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI/UX 개선&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다크 모드 지원&lt;/li&gt;
&lt;li&gt;반응형 디자인 강화&lt;/li&gt;
&lt;li&gt;키보드 단축키 지원&lt;/li&gt;
&lt;li&gt;상품 검색 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 스토리지 저장&lt;/li&gt;
&lt;li&gt;상품 데이터베이스 연동&lt;/li&gt;
&lt;li&gt;거래 내역 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 소스 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 구현 코드는 아래와 같습니다:&lt;/p&gt;
&lt;pre id=&quot;code_1729642412792&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Trash2, Plus } from 'lucide-react';

const MartCalculator = () =&amp;gt; {
  const [items, setItems] = useState([]);
  const [currentItem, setCurrentItem] = useState({
    name: '',
    price: '',
    quantity: '1',
    discount: '0'
  });
  const [total, setTotal] = useState(0);

  const addItem = () =&amp;gt; {
    if (currentItem.name &amp;amp;&amp;amp; currentItem.price) {
      const price = parseFloat(currentItem.price);
      const quantity = parseInt(currentItem.quantity) || 1;
      const discount = parseFloat(currentItem.discount) || 0;
      
      const subtotal = (price * quantity) * (1 - discount / 100);
      
      const newItem = {
        ...currentItem,
        price: parseFloat(currentItem.price),
        quantity: parseInt(currentItem.quantity),
        discount: parseFloat(currentItem.discount),
        subtotal: subtotal
      };

      setItems([...items, newItem]);
      setCurrentItem({
        name: '',
        price: '',
        quantity: '1',
        discount: '0'
      });

      calculateTotal([...items, newItem]);
    }
  };

  const removeItem = (index) =&amp;gt; {
    const newItems = items.filter((_, i) =&amp;gt; i !== index);
    setItems(newItems);
    calculateTotal(newItems);
  };

  const calculateTotal = (currentItems) =&amp;gt; {
    const newTotal = currentItems.reduce((sum, item) =&amp;gt; sum + item.subtotal, 0);
    setTotal(newTotal);
  };

  const formatCurrency = (amount) =&amp;gt; {
    return new Intl.NumberFormat('ko-KR', {
      style: 'currency',
      currency: 'KRW'
    }).format(amount);
  };

  return (
    &amp;lt;Card className=&quot;w-full max-w-3xl mx-auto&quot;&amp;gt;
      &amp;lt;div className=&quot;p-6&quot;&amp;gt;
        &amp;lt;h2 className=&quot;text-2xl font-bold mb-6&quot;&amp;gt;마트 계산기&amp;lt;/h2&amp;gt;
        
        {/* 상품 입력 폼 */}
        &amp;lt;div className=&quot;grid grid-cols-12 gap-2 mb-4&quot;&amp;gt;
          &amp;lt;Input
            className=&quot;col-span-3&quot;
            placeholder=&quot;상품명&quot;
            value={currentItem.name}
            onChange={(e) =&amp;gt; setCurrentItem({...currentItem, name: e.target.value})}
          /&amp;gt;
          &amp;lt;Input
            className=&quot;col-span-3&quot;
            placeholder=&quot;가격&quot;
            type=&quot;number&quot;
            value={currentItem.price}
            onChange={(e) =&amp;gt; setCurrentItem({...currentItem, price: e.target.value})}
          /&amp;gt;
          &amp;lt;Input
            className=&quot;col-span-2&quot;
            placeholder=&quot;수량&quot;
            type=&quot;number&quot;
            value={currentItem.quantity}
            onChange={(e) =&amp;gt; setCurrentItem({...currentItem, quantity: e.target.value})}
          /&amp;gt;
          &amp;lt;Input
            className=&quot;col-span-2&quot;
            placeholder=&quot;할인 %&quot;
            type=&quot;number&quot;
            value={currentItem.discount}
            onChange={(e) =&amp;gt; setCurrentItem({...currentItem, discount: e.target.value})}
          /&amp;gt;
          &amp;lt;Button className=&quot;col-span-2&quot; onClick={addItem}&amp;gt;
            &amp;lt;Plus className=&quot;mr-1 h-4 w-4&quot; /&amp;gt; 추가
          &amp;lt;/Button&amp;gt;
        &amp;lt;/div&amp;gt;

        {/* 상품 목록 테이블 */}
        &amp;lt;div className=&quot;border rounded-lg overflow-hidden&quot;&amp;gt;
          &amp;lt;table className=&quot;w-full&quot;&amp;gt;
            &amp;lt;thead className=&quot;bg-gray-50&quot;&amp;gt;
              &amp;lt;tr&amp;gt;
                &amp;lt;th className=&quot;py-2 px-4 text-left&quot;&amp;gt;상품명&amp;lt;/th&amp;gt;
                &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;가격&amp;lt;/th&amp;gt;
                &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;수량&amp;lt;/th&amp;gt;
                &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;할인&amp;lt;/th&amp;gt;
                &amp;lt;th className=&quot;py-2 px-4 text-right&quot;&amp;gt;소계&amp;lt;/th&amp;gt;
                &amp;lt;th className=&quot;py-2 px-4 w-[50px]&quot;&amp;gt;&amp;lt;/th&amp;gt;
              &amp;lt;/tr&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;
              {items.map((item, index) =&amp;gt; (
                &amp;lt;tr key={index} className=&quot;border-t&quot;&amp;gt;
                  &amp;lt;td className=&quot;py-2 px-4&quot;&amp;gt;{item.name}&amp;lt;/td&amp;gt;
                  &amp;lt;td className=&quot;py-2 px-4 text-right&quot;&amp;gt;{formatCurrency(item.price)}&amp;lt;/td&amp;gt;
                  &amp;lt;td className=&quot;py-2 px-4 text-right&quot;&amp;gt;{item.quantity}&amp;lt;/td&amp;gt;
                  &amp;lt;td className=&quot;py-2 px-4 text-right&quot;&amp;gt;{item.discount}%&amp;lt;/td&amp;gt;
                  &amp;lt;td className=&quot;py-2 px-4 text-right&quot;&amp;gt;{formatCurrency(item.subtotal)}&amp;lt;/td&amp;gt;
                  &amp;lt;td className=&quot;py-2 px-4&quot;&amp;gt;
                    &amp;lt;Button 
                      variant=&quot;ghost&quot; 
                      size=&quot;icon&quot;
                      onClick={() =&amp;gt; removeItem(index)}
                    &amp;gt;
                      &amp;lt;Trash2 className=&quot;h-4 w-4&quot; /&amp;gt;
                    &amp;lt;/Button&amp;gt;
                  &amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
              ))}
              {items.length === 0 &amp;amp;&amp;amp; (
                &amp;lt;tr className=&quot;border-t&quot;&amp;gt;
                  &amp;lt;td colSpan={6} className=&quot;py-8 text-center text-gray-500&quot;&amp;gt;
                    상품이 없습니다.
                  &amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
              )}
            &amp;lt;/tbody&amp;gt;
          &amp;lt;/table&amp;gt;
        &amp;lt;/div&amp;gt;

        {/* 총액 표시 */}
        &amp;lt;div className=&quot;mt-4 flex justify-end&quot;&amp;gt;
          &amp;lt;div className=&quot;bg-blue-50 p-4 rounded-lg&quot;&amp;gt;
            &amp;lt;span className=&quot;text-lg font-semibold mr-2&quot;&amp;gt;총액:&amp;lt;/span&amp;gt;
            &amp;lt;span className=&quot;text-2xl font-bold text-blue-600&quot;&amp;gt;
              {formatCurrency(total)}
            &amp;lt;/span&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/Card&amp;gt;
  );
};

export default MartCalculator;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로젝트를 통해 우리는 실제 사용 가능한 마트 계산기를 구현해보았습니다. React의 상태 관리, 이벤트 처리, 그리고 UI 컴포넌트 구성을 실전적으로 학습할 수 있었습니다. 이 코드를 기반으로 여러분의 필요에 맞게 기능을 확장하고 개선할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React 공식 문서: &lt;a href=&quot;https://react.dev&quot;&gt;https://react.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tailwind CSS 문서: &lt;a href=&quot;https://tailwindcss.com&quot;&gt;https://tailwindcss.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;shadcn/ui 컴포넌트: &lt;a href=&quot;https://ui.shadcn.com&quot;&gt;https://ui.shadcn.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>언어/REACT</category>
      <category>React</category>
      <category>마트계산기</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/104</guid>
      <comments>https://devsite.tistory.com/entry/React%EB%A1%9C-%EB%A7%88%ED%8A%B8-%EA%B3%84%EC%82%B0%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry104comment</comments>
      <pubDate>Wed, 23 Oct 2024 09:14:04 +0900</pubDate>
    </item>
    <item>
      <title>[React] React로 공학용 계산기 만들기</title>
      <link>https://devsite.tistory.com/entry/React-React%EB%A1%9C-%EA%B3%B5%ED%95%99%EC%9A%A9-%EA%B3%84%EC%82%B0%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3537800.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Le9A2/btsKfbbeHUh/KKW43ssGmKHkI7MdEIoCNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Le9A2/btsKfbbeHUh/KKW43ssGmKHkI7MdEIoCNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Le9A2/btsKfbbeHUh/KKW43ssGmKHkI7MdEIoCNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLe9A2%2FbtsKfbbeHUh%2FKKW43ssGmKHkI7MdEIoCNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;289&quot; height=&quot;289&quot; data-filename=&quot;3537800.png&quot; data-origin-width=&quot;512&quot; data-origin-height=&quot;512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 React와 shadcn/ui를 사용하여 완전한 기능을 갖춘 공학용 계산기를 만들어보겠습니다. 이 튜토리얼을 통해 React의 상태 관리, 이벤트 처리, 그리고 UI 컴포넌트 구성 방법을 배울 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;프로젝트 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 필요한 의존성을 설치합니다:&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# shadcn/ui 컴포넌트 설치
npx create-next-app@latest my-calculator --typescript --tailwind --eslint
cd my-calculator
npx shadcn-ui@latest init&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;컴포넌트 구조&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 계산기는 단일 React 컴포넌트로 구현됩니다. 주요 기능은 다음과 같습니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 산술 연산 (덧셈, 뺄셈, 곱셈, 나눗셈)&lt;/li&gt;
&lt;li&gt;공학 계산 기능 (삼각함수, 로그, 제곱근)&lt;/li&gt;
&lt;li&gt;상수 값 (&amp;pi;, e)&lt;/li&gt;
&lt;li&gt;메모리 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;상태 관리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산기에는 4개의 주요 상태가 필요합니다:&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;const [display, setDisplay] = useState('0');          // 현재 화면에 표시되는 값
const [memory, setMemory] = useState(null);           // 이전 계산 값 저장
const [waitingForOperand, setWaitingForOperand] = useState(true);  // 새로운 숫자 입력 대기 상태
const [pendingOperator, setPendingOperator] = useState(null);      // 대기 중인 연산자&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;계산 로직 구현&lt;/b&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 숫자 입력 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const inputDigit = (digit) =&amp;gt; {
  if (waitingForOperand) {
    setDisplay(String(digit));
    setWaitingForOperand(false);
  } else {
    setDisplay(display === '0' ? String(digit) : display + digit);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 기본 연산 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const performOperation = (nextOperator) =&amp;gt; {
  const nextValue = parseFloat(display);

  const operations = {
    '+': (prevValue, nextValue) =&amp;gt; prevValue + nextValue,
    '-': (prevValue, nextValue) =&amp;gt; prevValue - nextValue,
    '&amp;times;': (prevValue, nextValue) =&amp;gt; prevValue * nextValue,
    '&amp;divide;': (prevValue, nextValue) =&amp;gt; prevValue / nextValue,
  };

  if (memory === null) {
    setMemory(nextValue);
  } else if (pendingOperator) {
    const currentValue = memory || 0;
    const computedValue = operations[pendingOperator](currentValue, nextValue);

    setMemory(computedValue);
    setDisplay(String(computedValue));
  }

  setWaitingForOperand(true);
  setPendingOperator(nextOperator);
};&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 특수 함수 처리&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const performSpecialOperation = (operation) =&amp;gt; {
  const currentValue = parseFloat(display);
  let result;

  switch (operation) {
    case 'sin':
      result = Math.sin(currentValue * Math.PI / 180);
      break;
    case 'cos':
      result = Math.cos(currentValue * Math.PI / 180);
      break;
    case 'tan':
      result = Math.tan(currentValue * Math.PI / 180);
      break;
    // ... 기타 특수 함수들
  }

  setDisplay(String(result));
  setWaitingForOperand(true);
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UI 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 shadcn/ui의 Card와 Button 컴포넌트를 사용하여 구현합니다. 계산기는 그리드 레이아웃을 사용하여 버튼들을 배치합니다.&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;return (
  &amp;lt;Card className=&quot;w-96&quot;&amp;gt;
    &amp;lt;CardContent className=&quot;p-4&quot;&amp;gt;
      {/* 디스플레이 */}
      &amp;lt;div className=&quot;mb-4&quot;&amp;gt;
        &amp;lt;input
          type=&quot;text&quot;
          value={display}
          readOnly
          className=&quot;w-full text-right p-2 text-2xl bg-gray-100 rounded&quot;
        /&amp;gt;
      &amp;lt;/div&amp;gt;

      {/* 버튼 그리드 */}
      &amp;lt;div className=&quot;grid grid-cols-4 gap-2&quot;&amp;gt;
        {/* 과학 기능 버튼 */}
        &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('sin')} variant=&quot;outline&quot;&amp;gt;sin&amp;lt;/Button&amp;gt;
        &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('cos')} variant=&quot;outline&quot;&amp;gt;cos&amp;lt;/Button&amp;gt;
        {/* ... 기타 버튼들 */}
      &amp;lt;/div&amp;gt;
    &amp;lt;/CardContent&amp;gt;
  &amp;lt;/Card&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;주요 기능 설명&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;숫자 입력&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 숫자 버튼을 클릭하면 &lt;code&gt;inputDigit&lt;/code&gt; 함수가 호출됩니다.&lt;/li&gt;
&lt;li&gt;현재 상태가 새로운 숫자 입력을 기다리는 중이라면 디스플레이를 새 숫자로 교체합니다.&lt;/li&gt;
&lt;li&gt;그렇지 않다면 현재 디스플레이에 새 숫자를 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;연산자 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;연산자 버튼을 클릭하면 &lt;code&gt;performOperation&lt;/code&gt; 함수가 호출됩니다.&lt;/li&gt;
&lt;li&gt;이전 계산 결과가 있다면 현재 입력된 숫자와 연산을 수행합니다.&lt;/li&gt;
&lt;li&gt;결과를 메모리에 저장하고 디스플레이를 업데이트합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;특수 함수&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sin, cos, tan 등의 특수 함수는 &lt;code&gt;performSpecialOperation&lt;/code&gt; 함수에서 처리됩니다.&lt;/li&gt;
&lt;li&gt;현재 디스플레이 값을 입력으로 사용하여 계산을 수행합니다.&lt;/li&gt;
&lt;li&gt;각도는 도(degree) 단위로 입력받아 라디안으로 변환하여 계산합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;사용자 경험 개선&lt;/b&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;시각적 피드백&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;버튼 클릭 시 시각적 피드백을 제공하기 위해 shadcn/ui의 버튼 스타일을 활용&lt;/li&gt;
&lt;li&gt;연산자 버튼은 다른 색상을 사용하여 구분&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0으로 나누기 등의 에러 상황 처리&lt;/li&gt;
&lt;li&gt;잘못된 입력에 대한 피드백 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;확장 가능성&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 계산기는 다음과 같은 방향으로 확장할 수 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;추가 공학 함수 구현 (역삼각함수, 지수함수 등)&lt;/li&gt;
&lt;li&gt;단위 변환 기능&lt;/li&gt;
&lt;li&gt;기록 기능 추가&lt;/li&gt;
&lt;li&gt;테마 지원 (다크 모드/라이트 모드)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;전체 소스 코드&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 소스 코드는 아래와 같습니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import React, { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';

const ScientificCalculator = () =&amp;gt; {
  const [display, setDisplay] = useState('0');
  const [memory, setMemory] = useState(null);
  const [waitingForOperand, setWaitingForOperand] = useState(true);
  const [pendingOperator, setPendingOperator] = useState(null);

  const clearAll = () =&amp;gt; {
    setDisplay('0');
    setMemory(null);
    setWaitingForOperand(true);
    setPendingOperator(null);
  };

  const inputDigit = (digit) =&amp;gt; {
    if (waitingForOperand) {
      setDisplay(String(digit));
      setWaitingForOperand(false);
    } else {
      setDisplay(display === '0' ? String(digit) : display + digit);
    }
  };

  const inputDecimal = () =&amp;gt; {
    if (waitingForOperand) {
      setDisplay('0.');
      setWaitingForOperand(false);
    } else if (display.indexOf('.') === -1) {
      setDisplay(display + '.');
    }
  };

  const performOperation = (nextOperator) =&amp;gt; {
    const nextValue = parseFloat(display);

    const operations = {
      '+': (prevValue, nextValue) =&amp;gt; prevValue + nextValue,
      '-': (prevValue, nextValue) =&amp;gt; prevValue - nextValue,
      '&amp;times;': (prevValue, nextValue) =&amp;gt; prevValue * nextValue,
      '&amp;divide;': (prevValue, nextValue) =&amp;gt; prevValue / nextValue,
    };

    if (memory === null) {
      setMemory(nextValue);
    } else if (pendingOperator) {
      const currentValue = memory || 0;
      const computedValue = operations[pendingOperator](currentValue, nextValue);

      setMemory(computedValue);
      setDisplay(String(computedValue));
    }

    setWaitingForOperand(true);
    setPendingOperator(nextOperator);
  };

  const performSpecialOperation = (operation) =&amp;gt; {
    const currentValue = parseFloat(display);
    let result;

    switch (operation) {
      case 'sin':
        result = Math.sin(currentValue * Math.PI / 180);
        break;
      case 'cos':
        result = Math.cos(currentValue * Math.PI / 180);
        break;
      case 'tan':
        result = Math.tan(currentValue * Math.PI / 180);
        break;
      case 'sqrt':
        result = Math.sqrt(currentValue);
        break;
      case 'log':
        result = Math.log10(currentValue);
        break;
      case 'ln':
        result = Math.log(currentValue);
        break;
      default:
        return;
    }

    setDisplay(String(result));
    setWaitingForOperand(true);
  };

  return (
    &amp;lt;Card className=&quot;w-96&quot;&amp;gt;
      &amp;lt;CardContent className=&quot;p-4&quot;&amp;gt;
        &amp;lt;div className=&quot;mb-4&quot;&amp;gt;
          &amp;lt;input
            type=&quot;text&quot;
            value={display}
            readOnly
            className=&quot;w-full text-right p-2 text-2xl bg-gray-100 rounded&quot;
          /&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div className=&quot;grid grid-cols-4 gap-2&quot;&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('sin')} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;sin&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('cos')} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;cos&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('tan')} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;tan&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('sqrt')} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;&amp;radic;&amp;lt;/Button&amp;gt;

          &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('log')} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;log&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performSpecialOperation('ln')} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;ln&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; setDisplay(String(Math.PI))} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;&amp;pi;&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; setDisplay(String(Math.E))} variant=&quot;outline&quot; className=&quot;text-sm&quot;&amp;gt;e&amp;lt;/Button&amp;gt;

          &amp;lt;Button onClick={() =&amp;gt; inputDigit(7)}&amp;gt;7&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; inputDigit(8)}&amp;gt;8&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; inputDigit(9)}&amp;gt;9&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performOperation('&amp;divide;')} variant=&quot;secondary&quot;&amp;gt;&amp;divide;&amp;lt;/Button&amp;gt;

          &amp;lt;Button onClick={() =&amp;gt; inputDigit(4)}&amp;gt;4&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; inputDigit(5)}&amp;gt;5&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; inputDigit(6)}&amp;gt;6&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performOperation('&amp;times;')} variant=&quot;secondary&quot;&amp;gt;&amp;times;&amp;lt;/Button&amp;gt;

          &amp;lt;Button onClick={() =&amp;gt; inputDigit(1)}&amp;gt;1&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; inputDigit(2)}&amp;gt;2&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; inputDigit(3)}&amp;gt;3&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performOperation('-')} variant=&quot;secondary&quot;&amp;gt;-&amp;lt;/Button&amp;gt;

          &amp;lt;Button onClick={() =&amp;gt; inputDigit(0)}&amp;gt;0&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={inputDecimal}&amp;gt;.&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performOperation('=')} variant=&quot;default&quot;&amp;gt;=&amp;lt;/Button&amp;gt;
          &amp;lt;Button onClick={() =&amp;gt; performOperation('+')} variant=&quot;secondary&quot;&amp;gt;+&amp;lt;/Button&amp;gt;

          &amp;lt;Button onClick={clearAll} variant=&quot;destructive&quot; className=&quot;col-span-4&quot;&amp;gt;Clear&amp;lt;/Button&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/CardContent&amp;gt;
    &amp;lt;/Card&amp;gt;
  );
};

export default ScientificCalculator;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언어/REACT</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/103</guid>
      <comments>https://devsite.tistory.com/entry/React-React%EB%A1%9C-%EA%B3%B5%ED%95%99%EC%9A%A9-%EA%B3%84%EC%82%B0%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry103comment</comments>
      <pubDate>Tue, 22 Oct 2024 13:44:53 +0900</pubDate>
    </item>
    <item>
      <title>[Python] PyInstaller 사용법: Python 스크립트를 실행 파일로 변환하기</title>
      <link>https://devsite.tistory.com/entry/Python-PyInstaller-%EC%82%AC%EC%9A%A9%EB%B2%95-Python-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A5%BC-%EC%8B%A4%ED%96%89-%ED%8C%8C%EC%9D%BC%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;economic-1050731_1280.jpg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mZYOW/btsJ2Ke1GQO/6uViLptOs32BL5kIc2lzW1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mZYOW/btsJ2Ke1GQO/6uViLptOs32BL5kIc2lzW1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mZYOW/btsJ2Ke1GQO/6uViLptOs32BL5kIc2lzW1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmZYOW%2FbtsJ2Ke1GQO%2F6uViLptOs32BL5kIc2lzW1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;305&quot; data-filename=&quot;economic-1050731_1280.jpg&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Python은 강력하고 유연한 프로그래밍 언어지만, 때로는 Python이 설치되지 않은 시스템에서도 프로그램을 실행해야 할 때가 있습니다. 이럴 때 PyInstaller가 큰 도움이 됩니다. PyInstaller는 Python 스크립트를 독립 실행 파일(.exe)로 변환해주는 도구입니다. 이 글에서는 PyInstaller의 기본 사용법과 간단한 예제를 통해 그 활용법을 알아보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;PyInstaller 소개&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyInstaller는 Python 애플리케이션과 그 의존성을 번들로 묶어 단일 패키지로 만들어주는 도구입니다. 이를 통해 Python이 설치되지 않은 시스템에서도 프로그램을 실행할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;PyInstaller 설치&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyInstaller는 pip를 통해 쉽게 설치할 수 있습니다. 명령 프롬프트에서 다음 명령어를 실행하세요:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install pyinstaller&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;기본 사용법&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyInstaller의 기본 사용법은 매우 간단합니다. 다음 단계를 따르세요:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;명령 프롬프트를 열고 Python 스크립트가 있는 디렉토리로 이동합니다.&lt;/li&gt;
&lt;li&gt;다음 명령어를 실행합니다:여기서 &lt;code&gt;your_script.py&lt;/code&gt;는 변환하려는 Python 스크립트의 이름입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyinstaller --onefile your_script.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;변환이 완료되면 &lt;code&gt;dist&lt;/code&gt; 폴더 안에 실행 파일(.exe)이 생성됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;--onefile&lt;/code&gt; 옵션은 모든 의존성을 포함한 단일 실행 파일을 생성합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;예제: 간단한 계산기 프로그램&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제 예제를 통해 PyInstaller의 사용법을 알아보겠습니다. 다음은 간단한 계산기 프로그램입니다:&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def divide(a, b):
    if b != 0:
        return a / b
    else:
        return &quot;Error: Division by zero&quot;

while True:
    print(&quot;\nSimple Calculator&quot;)
    print(&quot;1. Add&quot;)
    print(&quot;2. Subtract&quot;)
    print(&quot;3. Multiply&quot;)
    print(&quot;4. Divide&quot;)
    print(&quot;5. Exit&quot;)

    choice = input(&quot;Enter your choice (1-5): &quot;)

    if choice == '5':
        print(&quot;Goodbye!&quot;)
        break

    if choice in ('1', '2', '3', '4'):
        num1 = float(input(&quot;Enter first number: &quot;))
        num2 = float(input(&quot;Enter second number: &quot;))

        if choice == '1':
            print(&quot;Result:&quot;, add(num1, num2))
        elif choice == '2':
            print(&quot;Result:&quot;, subtract(num1, num2))
        elif choice == '3':
            print(&quot;Result:&quot;, multiply(num1, num2))
        elif choice == '4':
            print(&quot;Result:&quot;, divide(num1, num2))
    else:
        print(&quot;Invalid input. Please try again.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스크립트를 &lt;code&gt;calculator.py&lt;/code&gt;로 저장한 후, 다음 단계를 따릅니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;명령 프롬프트를 열고 &lt;code&gt;calculator.py&lt;/code&gt;가 있는 디렉토리로 이동합니다.&lt;/li&gt;
&lt;li&gt;다음 명령어를 실행합니다:&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pyinstaller --onefile calculator.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;변환이 완료되면 &lt;code&gt;dist&lt;/code&gt; 폴더 안에 &lt;code&gt;calculator.exe&lt;/code&gt; 파일이 생성됩니다.&lt;/li&gt;
&lt;li&gt;이제 &lt;code&gt;calculator.exe&lt;/code&gt;를 더블 클릭하거나 명령 프롬프트에서 실행할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;추가 PyInstaller 옵션&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyInstaller에는 다양한 옵션이 있습니다. 몇 가지 유용한 옵션을 소개합니다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;--noconsole&lt;/code&gt;: GUI 애플리케이션의 경우 콘솔 창을 숨깁니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--icon=&amp;lt;icon_file.ico&amp;gt;&lt;/code&gt;: 실행 파일의 아이콘을 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--name=&amp;lt;name&amp;gt;&lt;/code&gt;: 출력 파일의 이름을 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;brainfuck&quot;&gt;&lt;code&gt;pyinstaller --onefile --noconsole --icon=calc.ico --name=MyCalculator calculator.py&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyInstaller를 사용하면 Python 스크립트를 쉽게 독립 실행 파일로 변환할 수 있습니다. 이를 통해 Python이 설치되지 않은 시스템에서도 프로그램을 배포하고 실행할 수 있게 됩니다. 복잡한 프로젝트의 경우 추가적인 설정이 필요할 수 있지만, 기본적인 사용법은 매우 간단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PyInstaller를 활용하여 여러분의 Python 프로젝트를 더 많은 사용자에게 배포해보세요!&lt;/p&gt;</description>
      <category>언어/PyThon</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/102</guid>
      <comments>https://devsite.tistory.com/entry/Python-PyInstaller-%EC%82%AC%EC%9A%A9%EB%B2%95-Python-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%A5%BC-%EC%8B%A4%ED%96%89-%ED%8C%8C%EC%9D%BC%EB%A1%9C-%EB%B3%80%ED%99%98%ED%95%98%EA%B8%B0#entry102comment</comments>
      <pubDate>Sat, 12 Oct 2024 10:37:12 +0900</pubDate>
    </item>
    <item>
      <title>[React] React로 할 일 관리 앱 만들기: 10장 - 앱 배포하기</title>
      <link>https://devsite.tistory.com/entry/React-React%EB%A1%9C-%ED%95%A0-%EC%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-10%EC%9E%A5-%EC%95%B1-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XLnDY/btsJ3IUoH7e/MXCDAbi4EYipxzAzqYhnZK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XLnDY/btsJ3IUoH7e/MXCDAbi4EYipxzAzqYhnZK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XLnDY/btsJ3IUoH7e/MXCDAbi4EYipxzAzqYhnZK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXLnDY%2FbtsJ3IUoH7e%2FMXCDAbi4EYipxzAzqYhnZK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;383&quot; height=&quot;287&quot; data-filename=&quot;img1.daumcdn.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 드디어 마지막 챕터에 도달했습니다. 이번 포스트에서는 우리가 만든 할 일 관리 앱을 실제로 배포하는 방법을 알아보겠습니다. GitHub Pages와 Netlify, 두 가지 방법으로 배포하는 과정을 step by step으로 설명하겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. GitHub Pages로 배포하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Pages는 GitHub 저장소에서 직접 정적 웹사이트를 호스팅할 수 있는 무료 서비스입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.1. GitHub 저장소 생성&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub에 로그인하고 새 저장소를 생성합니다.&lt;/li&gt;
&lt;li&gt;저장소 이름을 &lt;code&gt;todo-app&lt;/code&gt;으로 지정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.2. 로컬 프로젝트를 GitHub 저장소에 연결&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;git init
git add .
git commit -m &quot;Initial commit&quot;
git remote add origin https://github.com/your-username/todo-app.git
git push -u origin main&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.3. gh-pages 패키지 설치&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;q&quot;&gt;&lt;code&gt;npm install --save-dev gh-pages&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.4. package.json 수정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;package.json&lt;/code&gt; 파일을 열고 다음 내용을 추가합니다:&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;homepage&quot;: &quot;https://your-username.github.io/todo-app&quot;,
  &quot;scripts&quot;: {
    &quot;predeploy&quot;: &quot;npm run build&quot;,
    &quot;deploy&quot;: &quot;gh-pages -d build&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.5. 앱 배포&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm run deploy&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 명령어를 실행하면 프로젝트가 빌드되고 &lt;code&gt;gh-pages&lt;/code&gt; 브랜치가 생성되어 GitHub에 푸시됩니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1.6. GitHub Pages 설정&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub 저장소 페이지로 이동합니다.&lt;/li&gt;
&lt;li&gt;'Settings' 탭을 클릭합니다.&lt;/li&gt;
&lt;li&gt;좌측 메뉴에서 'Pages'를 클릭합니다.&lt;/li&gt;
&lt;li&gt;'Source' 섹션에서 'gh-pages' 브랜치를 선택합니다.&lt;/li&gt;
&lt;li&gt;'Save' 버튼을 클릭합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;https://your-username.github.io/todo-app&lt;/code&gt;에서 여러분의 앱을 볼 수 있습니다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. Netlify로 배포하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netlify는 정적 웹사이트 호스팅을 제공하는 플랫폼으로, GitHub와의 연동이 매우 쉽습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.1. Netlify 계정 생성&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.netlify.com/&quot;&gt;Netlify 웹사이트&lt;/a&gt;에 접속하여 계정을 생성합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.2. 새 사이트 추가&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Netlify 대시보드에서 &quot;New site from Git&quot; 버튼을 클릭합니다.&lt;/li&gt;
&lt;li&gt;GitHub을 선택하고 권한을 부여합니다.&lt;/li&gt;
&lt;li&gt;배포하려는 저장소를 선택합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.3. 배포 설정&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Build command: &lt;code&gt;npm run build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Publish directory: &lt;code&gt;build&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.4. 배포&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Deploy site&quot; 버튼을 클릭하면 Netlify가 자동으로 여러분의 앱을 빌드하고 배포합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.5. 커스텀 도메인 설정 (선택사항)&lt;/b&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Netlify 대시보드에서 여러분의 사이트를 선택합니다.&lt;/li&gt;
&lt;li&gt;&quot;Domain settings&quot;를 클릭합니다.&lt;/li&gt;
&lt;li&gt;&quot;Add custom domain&quot;을 클릭하고 원하는 도메인을 입력합니다.&lt;/li&gt;
&lt;li&gt;DNS 설정을 따라 진행합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. 환경 변수 관리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 앱에 API 키와 같은 민감한 정보가 있다면, 환경 변수를 사용하여 관리해야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.1. 로컬 환경에서 환경 변수 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 루트 디렉토리에 &lt;code&gt;.env&lt;/code&gt; 파일을 생성하고 다음과 같이 작성합니다:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;REACT_APP_API_KEY=your_api_key_here&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.2. 환경 변수 사용&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 컴포넌트에서 다음과 같이 환경 변수를 사용할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;const apiKey = process.env.REACT_APP_API_KEY;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.3. 배포 환경에서 환경 변수 설정&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitHub Pages: GitHub 저장소의 Secrets에 환경 변수를 추가합니다.&lt;/li&gt;
&lt;li&gt;Netlify: Netlify 대시보드의 &quot;Build &amp;amp; deploy&quot; 섹션에서 환경 변수를 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축하합니다! 이제 여러분의 React 할 일 관리 앱이 온라인에 공개되었습니다. 이 과정에서 배운 내용을 정리해 보겠습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;GitHub Pages를 사용한 정적 웹사이트 호스팅&lt;/li&gt;
&lt;li&gt;Netlify를 사용한 지속적 배포(CD) 설정&lt;/li&gt;
&lt;li&gt;환경 변수를 통한 민감한 정보 관리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 과정에서 어려움은 없으셨나요? 어떤 배포 방식이 더 편리하다고 느끼셨나요? 여러분의 경험과 생각을 댓글로 공유해 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것으로 &quot;React로 할 일 관리 앱 만들기&quot; 시리즈가 모두 끝났습니다. 이 튜토리얼을 통해 React 앱의 개발부터 배포까지 전체 과정을 경험해 보셨습니다. 앞으로 여러분만의 멋진 React 프로젝트를 만들어 나가시길 바랍니다!&lt;/p&gt;</description>
      <category>언어/REACT</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/101</guid>
      <comments>https://devsite.tistory.com/entry/React-React%EB%A1%9C-%ED%95%A0-%EC%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-10%EC%9E%A5-%EC%95%B1-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0#entry101comment</comments>
      <pubDate>Fri, 11 Oct 2024 16:06:35 +0900</pubDate>
    </item>
    <item>
      <title>[React] React로 할 일 관리 앱 만들기: 9장 - 단위 테스트 작성하기</title>
      <link>https://devsite.tistory.com/entry/React%EB%A1%9C-%ED%95%A0-%EC%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-9%EC%9E%A5-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ALqE7/btsJ3qfb14J/jdFCs1AeRz4WmZVbyqX6H0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ALqE7/btsJ3qfb14J/jdFCs1AeRz4WmZVbyqX6H0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ALqE7/btsJ3qfb14J/jdFCs1AeRz4WmZVbyqX6H0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FALqE7%2FbtsJ3qfb14J%2FjdFCs1AeRz4WmZVbyqX6H0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;405&quot; height=&quot;304&quot; data-filename=&quot;img1.daumcdn.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 이번 포스트에서는 우리가 만든 할 일 관리 앱에 대한 단위 테스트를 작성하는 방법을 알아보겠습니다. Jest와 React Testing Library를 사용하여 컴포넌트, Redux 액션, 리듀서에 대한 테스트를 작성해 보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. 테스트 환경 설정&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Create React App으로 프로젝트를 만들었다면 Jest와 React Testing Library가 이미 설정되어 있습니다. 추가로 필요한 패키지들을 설치해 보겠습니다:&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 컴포넌트 테스트 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 TodoForm 컴포넌트에 대한 테스트를 작성해 보겠습니다. &lt;code&gt;src/components&lt;/code&gt; 폴더에 &lt;code&gt;TodoForm.test.js&lt;/code&gt; 파일을 생성합니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/components/TodoForm.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import TodoForm from './TodoForm';
import { addTodoAsync } from '../store/todoSlice';

const mockStore = configureStore([]);

describe('TodoForm', () =&amp;gt; {
  let store;

  beforeEach(() =&amp;gt; {
    store = mockStore({});
    store.dispatch = jest.fn();
  });

  test('renders input and submit button', () =&amp;gt; {
    render(
      &amp;lt;Provider store={store}&amp;gt;
        &amp;lt;TodoForm /&amp;gt;
      &amp;lt;/Provider&amp;gt;
    );

    expect(screen.getByPlaceholderText('새로운 할 일을 입력하세요')).toBeInTheDocument();
    expect(screen.getByText('추가')).toBeInTheDocument();
  });

  test('dispatches addTodoAsync action when form is submitted', () =&amp;gt; {
    render(
      &amp;lt;Provider store={store}&amp;gt;
        &amp;lt;TodoForm /&amp;gt;
      &amp;lt;/Provider&amp;gt;
    );

    const input = screen.getByPlaceholderText('새로운 할 일을 입력하세요');
    const submitButton = screen.getByText('추가');

    fireEvent.change(input, { target: { value: '새로운 할 일' } });
    fireEvent.click(submitButton);

    expect(store.dispatch).toHaveBeenCalledWith(addTodoAsync('새로운 할 일'));
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Redux 액션 및 리듀서 테스트 작성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Redux 액션과 리듀서에 대한 테스트를 작성해 보겠습니다. &lt;code&gt;src/store&lt;/code&gt; 폴더에 &lt;code&gt;todoSlice.test.js&lt;/code&gt; 파일을 생성합니다:&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// src/store/todoSlice.test.js
import todoReducer, {
  addTodoAsync,
  toggleTodoAsync,
  deleteTodoAsync,
  editTodoAsync
} from './todoSlice';
import { configureStore } from '@reduxjs/toolkit';

describe('todoSlice', () =&amp;gt; {
  let store;

  beforeEach(() =&amp;gt; {
    store = configureStore({
      reducer: {
        todos: todoReducer
      }
    });
  });

  test('should handle initial state', () =&amp;gt; {
    expect(store.getState().todos).toEqual({
      items: [],
      status: 'idle',
      error: null
    });
  });

  test('should handle addTodoAsync', async () =&amp;gt; {
    await store.dispatch(addTodoAsync('New Todo'));
    const todos = store.getState().todos.items;
    expect(todos.length).toBe(1);
    expect(todos[0].text).toBe('New Todo');
  });

  test('should handle toggleTodoAsync', async () =&amp;gt; {
    await store.dispatch(addTodoAsync('Test Todo'));
    const todoId = store.getState().todos.items[0].id;
    await store.dispatch(toggleTodoAsync(todoId));
    const toggledTodo = store.getState().todos.items[0];
    expect(toggledTodo.completed).toBe(true);
  });

  test('should handle deleteTodoAsync', async () =&amp;gt; {
    await store.dispatch(addTodoAsync('Test Todo'));
    const todoId = store.getState().todos.items[0].id;
    await store.dispatch(deleteTodoAsync(todoId));
    expect(store.getState().todos.items.length).toBe(0);
  });

  test('should handle editTodoAsync', async () =&amp;gt; {
    await store.dispatch(addTodoAsync('Test Todo'));
    const todoId = store.getState().todos.items[0].id;
    await store.dispatch(editTodoAsync({ id: todoId, text: 'Edited Todo' }));
    const editedTodo = store.getState().todos.items[0];
    expect(editedTodo.text).toBe('Edited Todo');
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 비동기 API 호출 테스트하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 API 호출을 테스트하기 위해 Jest의 mock 기능을 사용할 수 있습니다. &lt;code&gt;src/api&lt;/code&gt; 폴더에 &lt;code&gt;todoApi.test.js&lt;/code&gt; 파일을 생성합니다:&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;// src/api/todoApi.test.js
import {
  fetchTodos,
  addTodoApi,
  toggleTodoApi,
  deleteTodoApi,
  editTodoApi
} from './todoApi';

// API 함수들을 모의(mock)합니다
jest.mock('./todoApi');

describe('Todo API', () =&amp;gt; {
  test('fetchTodos should return todos', async () =&amp;gt; {
    const mockTodos = [{ id: 1, text: 'Test Todo', completed: false }];
    fetchTodos.mockResolvedValue(mockTodos);

    const todos = await fetchTodos();
    expect(todos).toEqual(mockTodos);
  });

  test('addTodoApi should add a new todo', async () =&amp;gt; {
    const newTodo = { id: 2, text: 'New Todo', completed: false };
    addTodoApi.mockResolvedValue(newTodo);

    const result = await addTodoApi('New Todo');
    expect(result).toEqual(newTodo);
  });

  test('toggleTodoApi should toggle todo completion status', async () =&amp;gt; {
    const toggledTodo = { id: 1, text: 'Test Todo', completed: true };
    toggleTodoApi.mockResolvedValue(toggledTodo);

    const result = await toggleTodoApi(1);
    expect(result).toEqual(toggledTodo);
  });

  test('deleteTodoApi should delete a todo', async () =&amp;gt; {
    deleteTodoApi.mockResolvedValue(1);

    const result = await deleteTodoApi(1);
    expect(result).toBe(1);
  });

  test('editTodoApi should edit a todo', async () =&amp;gt; {
    const editedTodo = { id: 1, text: 'Edited Todo', completed: false };
    editTodoApi.mockResolvedValue(editedTodo);

    const result = await editTodoApi(1, 'Edited Todo');
    expect(result).toEqual(editedTodo);
  });
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 테스트 실행하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 테스트를 실행하려면 터미널에서 다음 명령어를 실행하세요:&lt;/p&gt;
&lt;pre class=&quot;bash&quot;&gt;&lt;code&gt;npm test&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 테스트 파일만 실행하려면 파일 이름을 지정할 수 있습니다:&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;npm test TodoForm.test.js&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서는 React 컴포넌트, Redux 액션 및 리듀서, 그리고 비동기 API 호출에 대한 단위 테스트를 작성하는 방법을 배웠습니다. 테스트를 작성함으로써 얻을 수 있는 장점은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;버그 조기 발견: 코드 변경 시 발생할 수 있는 버그를 빠르게 찾아낼 수 있습니다.&lt;/li&gt;
&lt;li&gt;리팩토링 용이성: 기존 기능이 올바르게 동작하는지 확인하며 코드를 개선할 수 있습니다.&lt;/li&gt;
&lt;li&gt;문서화: 테스트는 코드의 동작 방식을 설명하는 또 다른 형태의 문서가 됩니다.&lt;/li&gt;
&lt;li&gt;개발 속도 향상: 장기적으로 버그 수정 시간을 줄이고 개발 속도를 높일 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 작성에 대해 어떻게 생각하시나요? 테스트 작성이 개발 과정에 도움이 된다고 느끼시나요? 여러분의 경험과 생각을 댓글로 공유해 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 챕터에서는 이 할 일 관리 앱을 배포하는 방법에 대해 알아보겠습니다. GitHub Pages나 Netlify 같은 플랫폼을 사용하여 우리의 앱을 온라인에 공개하는 과정을 다룰 예정입니다. React 앱 개발과 테스트에 대해 궁금한 점이 있다면 언제든 질문해 주세요!&lt;/p&gt;</description>
      <category>언어/REACT</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/100</guid>
      <comments>https://devsite.tistory.com/entry/React%EB%A1%9C-%ED%95%A0-%EC%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-9%EC%9E%A5-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0#entry100comment</comments>
      <pubDate>Fri, 11 Oct 2024 16:04:16 +0900</pubDate>
    </item>
    <item>
      <title>[React] React로 할 일 관리 앱 만들기: 8장 - 비동기 작업과 미들웨어</title>
      <link>https://devsite.tistory.com/entry/React-React%EB%A1%9C-%ED%95%A0-%EC%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-8%EC%9E%A5-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EA%B3%BC-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img1.daumcdn.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byEe7j/btsJ2RrabDM/k4sO46SIAsIct72eKINOi1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byEe7j/btsJ2RrabDM/k4sO46SIAsIct72eKINOi1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byEe7j/btsJ2RrabDM/k4sO46SIAsIct72eKINOi1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyEe7j%2FbtsJ2RrabDM%2Fk4sO46SIAsIct72eKINOi1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;372&quot; height=&quot;279&quot; data-filename=&quot;img1.daumcdn.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 이번 포스트에서는 우리의 할 일 관리 앱에 비동기 작업 처리 기능을 추가하고, Redux Thunk 미들웨어를 사용하는 방법을 알아보겠습니다. 이를 통해 서버와의 통신을 시뮬레이션하고, 실제 애플리케이션에서 API 호출을 어떻게 처리할 수 있는지 배워보겠습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;1. Redux Thunk 설치하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Redux Thunk를 설치합니다. 터미널에서 다음 명령어를 실행하세요:&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install redux-thunk&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;2. 가짜 API 서비스 만들기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서버 없이 비동기 작업을 시뮬레이션하기 위해 가짜 API 서비스를 만들어 보겠습니다. &lt;code&gt;src&lt;/code&gt; 폴더에 &lt;code&gt;api&lt;/code&gt; 폴더를 만들고, 그 안에 &lt;code&gt;todoApi.js&lt;/code&gt; 파일을 생성합니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/api/todoApi.js

// 가짜 지연 함수
const delay = (ms) =&amp;gt; new Promise(resolve =&amp;gt; setTimeout(resolve, ms));

// 가짜 데이터베이스
let todos = [];

export const fetchTodos = async () =&amp;gt; {
  await delay(500); // 0.5초 지연
  return [...todos];
};

export const addTodoApi = async (text) =&amp;gt; {
  await delay(500);
  const newTodo = { id: Date.now(), text, completed: false };
  todos.push(newTodo);
  return newTodo;
};

export const toggleTodoApi = async (id) =&amp;gt; {
  await delay(500);
  const todo = todos.find(t =&amp;gt; t.id === id);
  if (todo) {
    todo.completed = !todo.completed;
    return { ...todo };
  }
  throw new Error('Todo not found');
};

export const deleteTodoApi = async (id) =&amp;gt; {
  await delay(500);
  const index = todos.findIndex(t =&amp;gt; t.id === id);
  if (index !== -1) {
    todos = todos.filter(t =&amp;gt; t.id !== id);
    return id;
  }
  throw new Error('Todo not found');
};

export const editTodoApi = async (id, text) =&amp;gt; {
  await delay(500);
  const todo = todos.find(t =&amp;gt; t.id === id);
  if (todo) {
    todo.text = text;
    return { ...todo };
  }
  throw new Error('Todo not found');
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;3. Redux Thunk 액션 생성하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;todoSlice.js&lt;/code&gt; 파일을 수정하여 비동기 액션을 추가합니다:&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;// src/store/todoSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { fetchTodos, addTodoApi, toggleTodoApi, deleteTodoApi, editTodoApi } from '../api/todoApi';

export const fetchTodosAsync = createAsyncThunk(
  'todos/fetchTodos',
  async () =&amp;gt; {
    const response = await fetchTodos();
    return response;
  }
);

export const addTodoAsync = createAsyncThunk(
  'todos/addTodo',
  async (text) =&amp;gt; {
    const response = await addTodoApi(text);
    return response;
  }
);

export const toggleTodoAsync = createAsyncThunk(
  'todos/toggleTodo',
  async (id) =&amp;gt; {
    const response = await toggleTodoApi(id);
    return response;
  }
);

export const deleteTodoAsync = createAsyncThunk(
  'todos/deleteTodo',
  async (id) =&amp;gt; {
    await deleteTodoApi(id);
    return id;
  }
);

export const editTodoAsync = createAsyncThunk(
  'todos/editTodo',
  async ({ id, text }) =&amp;gt; {
    const response = await editTodoApi(id, text);
    return response;
  }
);

const todoSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle',
    error: null
  },
  reducers: {},
  extraReducers: (builder) =&amp;gt; {
    builder
      .addCase(fetchTodosAsync.pending, (state) =&amp;gt; {
        state.status = 'loading';
      })
      .addCase(fetchTodosAsync.fulfilled, (state, action) =&amp;gt; {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchTodosAsync.rejected, (state, action) =&amp;gt; {
        state.status = 'failed';
        state.error = action.error.message;
      })
      .addCase(addTodoAsync.fulfilled, (state, action) =&amp;gt; {
        state.items.push(action.payload);
      })
      .addCase(toggleTodoAsync.fulfilled, (state, action) =&amp;gt; {
        const todo = state.items.find(todo =&amp;gt; todo.id === action.payload.id);
        if (todo) {
          todo.completed = action.payload.completed;
        }
      })
      .addCase(deleteTodoAsync.fulfilled, (state, action) =&amp;gt; {
        state.items = state.items.filter(todo =&amp;gt; todo.id !== action.payload);
      })
      .addCase(editTodoAsync.fulfilled, (state, action) =&amp;gt; {
        const todo = state.items.find(todo =&amp;gt; todo.id === action.payload.id);
        if (todo) {
          todo.text = action.payload.text;
        }
      });
  },
});

export default todoSlice.reducer;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;4. 컴포넌트 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 각 컴포넌트를 수정하여 새로운 비동기 액션을 사용하도록 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TodoList 컴포넌트&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;// src/components/TodoList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchTodosAsync } from '../store/todoSlice';
import TodoForm from './TodoForm';
import TodoItem from './TodoItem';
import TodoFilters from './TodoFilters';
import TodoStats from './TodoStats';
import './TodoList.css';

function TodoList() {
  const dispatch = useDispatch();
  const { items: todos, status, error } = useSelector(state =&amp;gt; state.todos);
  const [filter, setFilter] = React.useState('all');

  useEffect(() =&amp;gt; {
    if (status === 'idle') {
      dispatch(fetchTodosAsync());
    }
  }, [status, dispatch]);

  const filteredTodos = todos.filter((todo) =&amp;gt; {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  if (status === 'loading') {
    return &amp;lt;div&amp;gt;로딩 중...&amp;lt;/div&amp;gt;;
  }

  if (status === 'failed') {
    return &amp;lt;div&amp;gt;에러: {error}&amp;lt;/div&amp;gt;;
  }

  return (
    &amp;lt;div className=&quot;todo-app&quot;&amp;gt;
      &amp;lt;h2&amp;gt;할 일 관리 앱&amp;lt;/h2&amp;gt;
      &amp;lt;TodoForm /&amp;gt;
      &amp;lt;TodoFilters filter={filter} setFilter={setFilter} /&amp;gt;
      &amp;lt;ul className=&quot;todo-list&quot;&amp;gt;
        {filteredTodos.map((todo) =&amp;gt; (
          &amp;lt;TodoItem key={todo.id} todo={todo} /&amp;gt;
        ))}
      &amp;lt;/ul&amp;gt;
      &amp;lt;TodoStats /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

export default TodoList;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;TodoForm 컴포넌트&lt;/b&gt;&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/components/TodoForm.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodoAsync } from '../store/todoSlice';

function TodoForm() {
  const [inputValue, setInputValue] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = (e) =&amp;gt; {
    e.preventDefault();
    if (inputValue.trim() !== '') {
      dispatch(addTodoAsync(inputValue));
      setInputValue('');
    }
  };

  return (
    &amp;lt;form className=&quot;todo-form&quot; onSubmit={handleSubmit}&amp;gt;
      &amp;lt;input
        type=&quot;text&quot;
        value={inputValue}
        onChange={(e) =&amp;gt; setInputValue(e.target.value)}
        placeholder=&quot;새로운 할 일을 입력하세요&quot;
      /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;추가&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  );
}

export default TodoForm;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TodoItem 컴포넌트&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// src/components/TodoItem.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { toggleTodoAsync, deleteTodoAsync, editTodoAsync } from '../store/todoSlice';

function TodoItem({ todo }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editValue, setEditValue] = useState(todo.text);
  const dispatch = useDispatch();

  const handleEdit = () =&amp;gt; {
    dispatch(editTodoAsync({ id: todo.id, text: editValue }));
    setIsEditing(false);
  };

  return (
    &amp;lt;li className=&quot;todo-item&quot;&amp;gt;
      &amp;lt;input
        type=&quot;checkbox&quot;
        checked={todo.completed}
        onChange={() =&amp;gt; dispatch(toggleTodoAsync(todo.id))}
      /&amp;gt;
      {isEditing ? (
        &amp;lt;input
          type=&quot;text&quot;
          value={editValue}
          onChange={(e) =&amp;gt; setEditValue(e.target.value)}
          onBlur={handleEdit}
          autoFocus
        /&amp;gt;
      ) : (
        &amp;lt;span
          style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          onDoubleClick={() =&amp;gt; setIsEditing(true)}
        &amp;gt;
          {todo.text}
        &amp;lt;/span&amp;gt;
      )}
      &amp;lt;button onClick={() =&amp;gt; dispatch(deleteTodoAsync(todo.id))}&amp;gt;삭제&amp;lt;/button&amp;gt;
    &amp;lt;/li&amp;gt;
  );
}

export default TodoItem;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;TodoStats 컴포넌트&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TodoStats 컴포넌트는 변경할 필요가 없습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;5. 스토어 설정 수정하기&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;src/store/store.js&lt;/code&gt; 파일을 수정하여 Redux Thunk 미들웨어를 추가합니다:&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice';

const store = configureStore({
  reducer: {
    todos: todoReducer,
  },
  // Redux Toolkit의 configureStore는 기본적으로 Redux Thunk를 포함합니다.
});

export default store;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;실행 결과&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &lt;code&gt;npm start&lt;/code&gt; 명령어로 앱을 실행하면, 비동기 작업을 처리하는 할 일 관리 앱을 볼 수 있습니다. 각 작업(추가, 삭제, 수정, 완료 상태 변경)에 약간의 지연이 있을 것입니다. 이는 실제 서버 통신을 시뮬레이션한 것입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 챕터에서는 Redux Thunk를 사용하여 비동기 작업을 처리하는 방법을 배웠습니다. 이를 통해 얻을 수 있는 장점은 다음과 같습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비동기 로직 분리: 컴포넌트에서 비동기 로직을 분리하여 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;상태 업데이트 일관성: 서버의 응답을 기반으로 상태를 업데이트하여 일관성을 유지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;에러 처리: 비동기 작업의 실패를 쉽게 처리하고 사용자에게 알릴 수 있습니다.&lt;/li&gt;
&lt;li&gt;로딩 상태 관리: 데이터를 불러오는 동안 로딩 상태를 쉽게 관리할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redux Thunk를 사용하면서 느낀 점이 있나요? 비동기 작업 처리가 더 쉬워졌나요, 아니면 더 복잡해졌나요? 여러분의 경험을 댓글로 공유해 주세요!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 챕터에서는 단위 테스트 작성 방법에 대해 알아보겠습니다. React, Redux, 그리고 비동기 작업을 포함한 애플리케이션을 어떻게 테스트할 수 있는지 살펴볼 예정입니다. React와 Redux로 앱을 개발하면서 궁금한 점이 있다면 언제든 질문해 주세요!&lt;/p&gt;</description>
      <category>언어/REACT</category>
      <author>shaprimanDev</author>
      <guid isPermaLink="true">https://devsite.tistory.com/99</guid>
      <comments>https://devsite.tistory.com/entry/React-React%EB%A1%9C-%ED%95%A0-%EC%9D%BC-%EA%B4%80%EB%A6%AC-%EC%95%B1-%EB%A7%8C%EB%93%A4%EA%B8%B0-8%EC%9E%A5-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EA%B3%BC-%EB%AF%B8%EB%93%A4%EC%9B%A8%EC%96%B4#entry99comment</comments>
      <pubDate>Fri, 11 Oct 2024 16:02:52 +0900</pubDate>
    </item>
  </channel>
</rss>