Avatar
πŸ˜‰

Organizations

  • Ubuntuμ—μ„œ PostgreSQL을 μ„€μΉ˜ν•˜κ³  μ™ΈλΆ€ 접속을 ν—ˆμš©ν•˜λ €λ©΄ λ‹€μŒκ³Ό 같은 λ‹¨κ³„λ‘œ μ§„ν–‰ν•˜λ©΄ λ©λ‹ˆλ‹€.


    1. PostgreSQL μ„€μΉ˜

    sudo apt update
    sudo apt install postgresql postgresql-contrib
    

    μ„€μΉ˜ ν›„ PostgreSQL μ„œλΉ„μŠ€κ°€ μžλ™ μ‹œμž‘λ©λ‹ˆλ‹€. μƒνƒœ 확인:

    sudo systemctl status postgresql
    

    2. PostgreSQL μœ μ € 및 DB μ„€μ •

    PostgreSQL μ„€μΉ˜ μ‹œ 기본적으둜 postgresλΌλŠ” μœ μ €κ°€ μƒμ„±λ©λ‹ˆλ‹€.

    sudo -i -u postgres
    psql
    

    μ›ν•˜λŠ” μœ μ €μ™€ DBλ₯Ό λ§Œλ“€κ³  λΉ„λ°€λ²ˆν˜Έ μ„€μ •:

    CREATE USER myuser WITH PASSWORD 'mypassword';
    CREATE DATABASE mydb OWNER myuser;
    

    κΆŒν•œ λΆ€μ—¬:

    Created Mon, 23 Jun 2025 10:14:00 +0900
  • 컀링(Currying)은 μ—¬λŸ¬ 개의 인자λ₯Ό λ°›λŠ” ν•¨μˆ˜λ₯Ό ν•˜λ‚˜μ˜ 인자만 λ°›λŠ” ν•¨μˆ˜λ“€μ˜ μ—°μ†μœΌλ‘œ λ³€ν™˜ν•˜λŠ” κΈ°λ²•μž…λ‹ˆλ‹€. μˆ˜ν•™μž ν•˜μŠ€μΌˆ 컀리(Haskell Curry)의 μ΄λ¦„μ—μ„œ λ”°μ˜¨ μš©μ–΄μž…λ‹ˆλ‹€.

    κΈ°λ³Έ κ°œλ…

    일반적인 ν•¨μˆ˜:

    function add(a, b, c) {
        return a + b + c;
    }
    
    add(1, 2, 3); // 6
    

    컀링된 ν•¨μˆ˜:

    function curriedAdd(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            };
        };
    }
    
    curriedAdd(1)(2)(3); // 6
    

    ν™”μ‚΄ν‘œ ν•¨μˆ˜λ‘œ κ°„λ‹¨ν•˜κ²Œ

    const curriedAdd = a => b => c => a + b + c;
    
    const result = curriedAdd(1)(2)(3); // 6
    

    λΆ€λΆ„ 적용 (Partial Application)

    컀링의 κ°€μž₯ 큰 μž₯점은 λΆ€λΆ„ 적용이 κ°€λŠ₯ν•˜λ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€:

    const curriedAdd = a => b => c => a + b + c;
    
    // λΆ€λΆ„ 적용
    const add5 = curriedAdd(5);
    const add5And3 = add5(3);
    
    console.log(add5And3(2)); // 10 (5 + 3 + 2)
    console.log(add5(4)(1)); // 10 (5 + 4 + 1)
    

    μ‹€μš©μ μΈ μ˜ˆμ œλ“€

    1. λ¬Έμžμ—΄ 검사 ν•¨μˆ˜

    const hasSubstring = substring => str => str.includes(substring);
    
    const hasReact = hasSubstring('React');
    const hasVue = hasSubstring('Vue');
    
    console.log(hasReact('React 개발자')); // true
    console.log(hasVue('Vue.js ν”„λ ˆμž„μ›Œν¬')); // true
    
    // λ°°μ—΄μ—μ„œ ν™œμš©
    const frameworks = ['React', 'Angular', 'Vue', 'Svelte'];
    const reactItems = frameworks.filter(hasReact);
    console.log(reactItems); // ['React']
    

    2. μˆ˜ν•™ μ—°μ‚°

    const multiply = a => b => a * b;
    const divide = a => b => a / b;
    
    const double = multiply(2);
    const triple = multiply(3);
    const half = divide(1/2); // λ˜λŠ” multiply(0.5)
    
    console.log(double(5)); // 10
    console.log(triple(4)); // 12
    
    const numbers = [1, 2, 3, 4, 5];
    const doubled = numbers.map(double);
    console.log(doubled); // [2, 4, 6, 8, 10]
    

    3. 이벀트 ν•Έλ“€λŸ¬

    const log = level => message => {
        console.log(`[${level}] ${message}`);
    };
    
    const logError = log('ERROR');
    const logInfo = log('INFO');
    
    logError('λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ‹€νŒ¨'); // [ERROR] λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ‹€νŒ¨
    logInfo('μ„œλ²„ μ‹œμž‘λ¨'); // [INFO] μ„œλ²„ μ‹œμž‘λ¨
    

    4. API μš”μ²­

    const apiRequest = method => url => data => {
        return fetch(url, {
            method: method,
            headers: { 'Content-Type': 'application/json' },
            body: data ? JSON.stringify(data) : undefined
        });
    };
    
    const get = apiRequest('GET');
    const post = apiRequest('POST');
    const put = apiRequest('PUT');
    
    // μ‚¬μš©
    get('/api/users')();
    post('/api/users')({ name: 'κΉ€μ² μˆ˜', email: 'kim@example.com' });
    

    μžλ™ 컀링 ν•¨μˆ˜ λ§Œλ“€κΈ°

    function curry(fn) {
        return function curried(...args) {
            if (args.length >= fn.length) {
                return fn.apply(this, args);
            } else {
                return function(...nextArgs) {
                    return curried.apply(this, args.concat(nextArgs));
                };
            }
        };
    }
    
    // μ‚¬μš© 예
    function add(a, b, c) {
        return a + b + c;
    }
    
    const curriedAdd = curry(add);
    
    console.log(curriedAdd(1)(2)(3)); // 6
    console.log(curriedAdd(1, 2)(3)); // 6
    console.log(curriedAdd(1)(2, 3)); // 6
    console.log(curriedAdd(1, 2, 3)); // 6
    

    λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œμ˜ ν™œμš©

    Lodash

    const _ = require('lodash');
    
    const add = (a, b, c) => a + b + c;
    const curriedAdd = _.curry(add);
    
    console.log(curriedAdd(1)(2)(3)); // 6
    

    Ramda

    const R = require('ramda');
    
    const users = [
        { name: 'κΉ€μ² μˆ˜', age: 25 },
        { name: '이영희', age: 30 },
        { name: 'λ°•λ―Όμˆ˜', age: 35 }
    ];
    
    const getProperty = R.curry((prop, obj) => obj[prop]);
    const getName = getProperty('name');
    
    const names = users.map(getName);
    console.log(names); // ['κΉ€μ² μˆ˜', '이영희', 'λ°•λ―Όμˆ˜']
    

    컀링의 μž₯점

    1. μž¬μ‚¬μš©μ„±: νŠΉμ • λ§€κ°œλ³€μˆ˜κ°€ κ³ μ •λœ μƒˆλ‘œμš΄ ν•¨μˆ˜λ₯Ό μ‰½κ²Œ 생성
    2. 가독성: ν•¨μˆ˜μ˜ μ˜λ„κ°€ 더 λͺ…확해짐
    3. ν•¨μˆ˜ μ‘°ν•©: λ‹€λ₯Έ κ³ μ°¨ ν•¨μˆ˜λ“€κ³Ό μ‘°ν•©ν•˜κΈ° 쉬움
    4. λΆ€λΆ„ 적용: ν•„μš”ν•  λ•Œ 일뢀 인자만 λ¨Όμ € 적용 κ°€λŠ₯

    μ£Όμ˜μ‚¬ν•­

    1. μ„±λŠ₯: ν•¨μˆ˜ 호좜이 μ€‘μ²©λ˜λ―€λ‘œ μ•½κ°„μ˜ μ˜€λ²„ν—€λ“œ λ°œμƒ
    2. 디버깅: μŠ€νƒ νŠΈλ ˆμ΄μŠ€κ°€ λ³΅μž‘ν•΄μ§ˆ 수 있음
    3. 가독성: κ³Όλ„ν•˜κ²Œ μ‚¬μš©ν•˜λ©΄ 였히렀 μ½”λ“œλ₯Ό μ΄ν•΄ν•˜κΈ° μ–΄λ €μ›Œμ§ˆ 수 있음

    컀링은 ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ˜ 핡심 κ°œλ… 쀑 ν•˜λ‚˜λ‘œ, μ½”λ“œμ˜ μž¬μ‚¬μš©μ„±κ³Ό λͺ¨λ“ˆμ„±μ„ 크게 ν–₯μƒμ‹œν‚¬ 수 μžˆλŠ” κ°•λ ₯ν•œ κΈ°λ²•μž…λ‹ˆλ‹€.

    Created Fri, 20 Jun 2025 14:11:00 +0900
  • 1. ν•¨μˆ˜ν˜• μΈν„°νŽ˜μ΄μŠ€ μ‚¬μš© (Java 8+)

    기본적인 컀링 κ΅¬ν˜„

    import java.util.function.Function;
    
    public class CurryingExample {
        
        // 3개 인자λ₯Ό λ°›λŠ” ν•¨μˆ˜λ₯Ό 컀링
        public static Function<Integer, Function<Integer, Function<Integer, Integer>>> 
               curriedAdd() {
            return a -> b -> c -> a + b + c;
        }
        
        public static void main(String[] args) {
            // μ‚¬μš© 예
            Function<Integer, Function<Integer, Function<Integer, Integer>>> add = curriedAdd();
            
            int result1 = add.apply(1).apply(2).apply(3); // 6
            System.out.println(result1);
            
            // λΆ€λΆ„ 적용
            Function<Integer, Function<Integer, Integer>> add5 = add.apply(5);
            Function<Integer, Integer> add5And3 = add5.apply(3);
            
            int result2 = add5And3.apply(2); // 10
            System.out.println(result2);
        }
    }
    

    더 λ³΅μž‘ν•œ 예제 - λ¬Έμžμ—΄ 처리

    import java.util.function.Function;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class StringCurrying {
        
        // λ¬Έμžμ—΄ 포함 검사 ν•¨μˆ˜
        public static Function<String, Function<String, Boolean>> contains() {
            return substring -> str -> str.contains(substring);
        }
        
        // λ¬Έμžμ—΄ λ³€ν™˜ ν•¨μˆ˜
        public static Function<String, Function<String, String>> replace() {
            return target -> replacement -> str -> str.replace(target, replacement);
        }
        
        public static void main(String[] args) {
            List<String> frameworks = Arrays.asList("React", "Angular", "Vue", "Svelte");
            
            // λΆ€λΆ„ 적용된 ν•¨μˆ˜ 생성
            Function<String, Boolean> hasReact = contains().apply("React");
            
            // 슀트림과 ν•¨κ»˜ μ‚¬μš©
            List<String> reactItems = frameworks.stream()
                .filter(hasReact::apply)
                .collect(Collectors.toList());
            
            System.out.println(reactItems); // [React]
            
            // λ¬Έμžμ—΄ μΉ˜ν™˜
            Function<String, String> replaceJavaWithKotlin = 
                replace().apply("Java").apply("Kotlin");
            
            String result = replaceJavaWithKotlin.apply("I love Java programming");
            System.out.println(result); // I love Kotlin programming
        }
    }
    

    2. μ œλ„€λ¦­μ„ ν™œμš©ν•œ λ²”μš© 컀링 μœ ν‹Έλ¦¬ν‹°

    import java.util.function.*;
    
    public class CurryUtil {
        
        // 2개 인자 ν•¨μˆ˜ 컀링
        public static <A, B, R> Function<A, Function<B, R>> 
               curry(BiFunction<A, B, R> function) {
            return a -> b -> function.apply(a, b);
        }
        
        // 3개 인자 ν•¨μˆ˜λ₯Ό μœ„ν•œ ν•¨μˆ˜ν˜• μΈν„°νŽ˜μ΄μŠ€
        @FunctionalInterface
        public interface TriFunction<A, B, C, R> {
            R apply(A a, B b, C c);
        }
        
        // 3개 인자 ν•¨μˆ˜ 컀링
        public static <A, B, C, R> Function<A, Function<B, Function<C, R>>> 
               curry(TriFunction<A, B, C, R> function) {
            return a -> b -> c -> function.apply(a, b, c);
        }
        
        public static void main(String[] args) {
            // BiFunction 컀링
            BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
            Function<Integer, Function<Integer, Integer>> curriedMultiply = curry(multiply);
            
            Function<Integer, Integer> double_ = curriedMultiply.apply(2);
            System.out.println(double_.apply(5)); // 10
            
            // TriFunction 컀링
            TriFunction<Integer, Integer, Integer, Integer> add3 = (a, b, c) -> a + b + c;
            Function<Integer, Function<Integer, Function<Integer, Integer>>> curriedAdd3 = curry(add3);
            
            int result = curriedAdd3.apply(1).apply(2).apply(3);
            System.out.println(result); // 6
        }
    }
    

    3. 클래슀 기반 κ΅¬ν˜„

    public class MathOperations {
        
        public static class CurriedAdd {
            private final int first;
            
            private CurriedAdd(int first) {
                this.first = first;
            }
            
            public static CurriedAdd of(int first) {
                return new CurriedAdd(first);
            }
            
            public CurriedAddSecond with(int second) {
                return new CurriedAddSecond(first, second);
            }
            
            public class CurriedAddSecond {
                private final int second;
                
                private CurriedAddSecond(int first, int second) {
                    this.second = second;
                }
                
                public int and(int third) {
                    return first + second + third;
                }
            }
        }
        
        public static void main(String[] args) {
            int result = CurriedAdd.of(1).with(2).and(3); // 6
            System.out.println(result);
            
            // λΆ€λΆ„ 적용
            CurriedAdd add5 = CurriedAdd.of(5);
            CurriedAdd.CurriedAddSecond add5And3 = add5.with(3);
            
            int result2 = add5And3.and(2); // 10
            System.out.println(result2);
        }
    }
    

    4. μ‹€μš©μ μΈ 예제 - HTTP ν΄λΌμ΄μ–ΈνŠΈ

    import java.util.function.Function;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.net.URI;
    
    public class CurriedHttpClient {
        
        public static Function<String, Function<String, Function<String, HttpRequest>>> 
               httpRequest() {
            return method -> url -> body -> {
                HttpRequest.Builder builder = HttpRequest.newBuilder()
                    .uri(URI.create(url));
                
                switch (method.toUpperCase()) {
                    case "GET":
                        builder.GET();
                        break;
                    case "POST":
                        builder.POST(HttpRequest.BodyPublishers.ofString(body));
                        break;
                    case "PUT":
                        builder.PUT(HttpRequest.BodyPublishers.ofString(body));
                        break;
                }
                
                return builder.build();
            };
        }
        
        public static void main(String[] args) {
            // λΆ€λΆ„ 적용된 ν•¨μˆ˜λ“€ 생성
            Function<String, Function<String, HttpRequest>> get = 
                httpRequest().apply("GET");
            Function<String, Function<String, HttpRequest>> post = 
                httpRequest().apply("POST");
            
            // μ‚¬μš©
            HttpRequest getRequest = get.apply("https://api.example.com/users").apply("");
            HttpRequest postRequest = post.apply("https://api.example.com/users")
                .apply("{\"name\":\"κΉ€μ² μˆ˜\"}");
            
            System.out.println("GET: " + getRequest.uri());
            System.out.println("POST: " + postRequest.uri());
        }
    }
    

    5. Vavr 라이브러리 μ‚¬μš©

    Vavr(이전 Javaslang)λŠ” μžλ°”μ—μ„œ ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ„ 더 μ‰½κ²Œ ν•  수 있게 ν•΄μ£ΌλŠ” λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€:

    Created Fri, 20 Jun 2025 14:11:00 +0900
  • JavaScriptμ—μ„œ **ν΄λ‘œμ €(Closure)**λŠ” ν•¨μˆ˜μ™€ κ·Έ ν•¨μˆ˜κ°€ μ„ μ–Έλœ μ–΄νœ˜μ  ν™˜κ²½(Lexical Environment)의 쑰합을 λ§ν•©λ‹ˆλ‹€. 즉, ν•¨μˆ˜κ°€ μ™ΈλΆ€ μŠ€μ½”ν”„μ˜ λ³€μˆ˜μ— μ ‘κ·Όν•  수 μžˆλŠ” κΈ°λŠ₯을 μ˜λ―Έν•©λ‹ˆλ‹€.


    πŸ“Œ κΈ°λ³Έ κ°œλ…

    ν΄λ‘œμ €λŠ” λ‹€μŒκ³Ό 같은 μ‘°κ±΄μ—μ„œ μƒμ„±λ©λ‹ˆλ‹€:

    1. ν•¨μˆ˜ μ•ˆμ— ν•¨μˆ˜κ°€ μ •μ˜λ˜μ–΄ 있고,
    2. λ‚΄λΆ€ ν•¨μˆ˜κ°€ μ™ΈλΆ€ ν•¨μˆ˜μ˜ λ³€μˆ˜μ— μ ‘κ·Όν•  수 있으며,
    3. μ™ΈλΆ€ ν•¨μˆ˜μ˜ 싀행이 λλ‚œ 뒀에도 λ‚΄λΆ€ ν•¨μˆ˜κ°€ μ—¬μ „νžˆ κ·Έ λ³€μˆ˜μ— μ ‘κ·Όν•  수 μžˆμ„ λ•Œ ν΄λ‘œμ €κ°€ λ©λ‹ˆλ‹€.
    function outer() {
      let count = 0;
      return function inner() {
        count++;
        console.log(count);
      }
    }
    
    const counter = outer();
    counter(); // 1
    counter(); // 2
    

    μœ„ μ˜ˆμ œμ—μ„œ inner() ν•¨μˆ˜λŠ” outer()의 μ§€μ—­ λ³€μˆ˜ count에 μ ‘κ·Όν•©λ‹ˆλ‹€. outer()의 싀행이 λλ‚¬μŒμ—λ„ counter()κ°€ count에 계속 μ ‘κ·Όν•  수 μžˆλŠ” μ΄μœ λŠ” ν΄λ‘œμ € λ•Œλ¬Έμž…λ‹ˆλ‹€.

    Created Fri, 20 Jun 2025 14:06:17 +0900
  • MySQL을 λΉ„λ‘―ν•œ λŒ€λΆ€λΆ„μ˜ κ΄€κ³„ν˜• λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ μ‚¬μš©λ˜λŠ” JOIN은 μ—¬λŸ¬ ν…Œμ΄λΈ”μ˜ 데이터λ₯Ό μ‘°ν•©ν•΄ ν•˜λ‚˜μ˜ 결과둜 λ§Œλ“œλŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. μ•„λž˜λŠ” λŒ€ν‘œμ μΈ JOIN μ’…λ₯˜μ™€ 각각의 μ„€λͺ…, 예제λ₯Ό ν¬ν•¨ν•œ μ •λ¦¬μž…λ‹ˆλ‹€.


    πŸ”— 1. INNER JOIN

    βœ… μ •μ˜

    두 ν…Œμ΄λΈ”μ—μ„œ κ³΅ν†΅λœ 값이 μ‘΄μž¬ν•˜λŠ” ν–‰λ§Œ λ°˜ν™˜ν•©λ‹ˆλ‹€.

    πŸ§ͺ μ˜ˆμ‹œ

    SELECT a.id, a.name, b.order_date
    FROM customers a
    INNER JOIN orders b ON a.id = b.customer_id;
    

    πŸ“Œ νŠΉμ§•

    • κ°€μž₯ 일반적인 JOIN 방식
    • μ–‘μͺ½ λͺ¨λ‘ μΌμΉ˜ν•˜λŠ” 값이 μžˆμ–΄μ•Ό 결과에 포함됨

    πŸ”— 2. LEFT JOIN (λ˜λŠ” LEFT OUTER JOIN)

    βœ… μ •μ˜

    μ™Όμͺ½ ν…Œμ΄λΈ”μ˜ λͺ¨λ“  행을 λ°˜ν™˜ν•˜λ©°, 였λ₯Έμͺ½ ν…Œμ΄λΈ”μ— μΌμΉ˜ν•˜λŠ” 값이 μ—†μœΌλ©΄ NULL둜 μ±„μ›Œμ§‘λ‹ˆλ‹€.

    Created Wed, 18 Jun 2025 11:27:58 +0900
  • HttpApiCachingFilter 와 CachedHttpServletRequestWrapper

    • jsonκ³Ό multipart μ™Έ form-urlencoded의 κ²½μš°λ„ ν—ˆμš©ν•˜λ©΄μ„œ logging 정보 λ‚¨κΈ°λŠ” filter κ΅¬ν˜„ μ˜ˆμ‹œ
    • request 의 inputStream을 ν•œλ²ˆ 읽으면 휘발되기 λ•Œλ¬Έμ— 이λ₯Ό λ°©μ§€ν•˜λŠ” 둜직이 μ€‘μš”ν•©λ‹ˆλ‹€.

    HttpApiCachingFilter

    @Component
    @Order(value = Ordered.HIGHEST_PRECEDENCE)
    @WebFilter(filterName = "HttpApiCachingFilter", urlPatterns = "/*")
    @Slf4j
    public class HttpApiCachingFilter extends OncePerRequestFilter {
    
        private static String APPLICATION_NAME;
    
        @Value("${spring.application.name}")
        private void setApplicationName(String value) {
            APPLICATION_NAME = value;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
            if (isAsyncDispatch(request)) {
                filterChain.doFilter(request, response);
            } else if (isMultipartRequest(request)) {
                doFilterWrapped(new StandardMultipartHttpServletRequest(request), new CachedHttpServletResponseWrapper(response), filterChain);
            } else {
                doFilterWrapped(new CachedHttpServletRequestWrapper(request), new CachedHttpServletResponseWrapper(response), filterChain);
            }
        }
    
        protected void doFilterWrapped(HttpServletRequestWrapper request, ContentCachingResponseWrapper response, FilterChain filterChain)
            throws ServletException, IOException {
            try {
                logRequestHeader(request);
                logRequest(request);
                filterChain.doFilter(request, response);
            } finally {
                logResponseHeader(response);
                logResponse(response);
                response.copyBodyToResponse();
            }
        }
    
        private static void logRequestHeader(HttpServletRequestWrapper request) {
            Enumeration<String> headerNames = request.getHeaderNames();
            if (headerNames != null) {
                Collections.list(headerNames).forEach(headersName -> {
                    String headerValue = request.getHeader(headersName);
                    log.info("Request Header {}: {}", headersName, headerValue);
                });
            }
        }
    
        private static void logRequest(HttpServletRequestWrapper request) throws IOException {
            String contentType = request.getContentType();
    
            log.info("Request body: {} uri=[{}] content-type=[{}]", request.getMethod(),
                request.getRequestURI(), contentType);
    
            if (contentType != null && contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
                Map<String, String[]> paramMap = request.getParameterMap();
                paramMap.forEach((key, values) -> {
                    for (String value : values) {
                        log.info("REQUEST Param: {} = {}", key, value);
                    }
                });
            } else {
                logPayload("REQUEST", contentType, request.getInputStream(), request.getRequestURI());
            }
        }
    
        private static void logResponseHeader(ContentCachingResponseWrapper response) {
            Collection<String> headerNames = response.getHeaderNames();
            for (String headerName : headerNames) {
                for (String value : response.getHeaders(headerName)) {
                    log.info("Response Header {}: {}", headerName, value);
                }
            }
        }
    
        private static void logResponse(ContentCachingResponseWrapper response) throws IOException {
            logPayload("RESPONSE", response.getContentType(), response.getContentInputStream(), null);
        }
    
        private static void logPayload(String direction,
                                       String contentType,
                                       InputStream inputStream,
                                       String targetUri
        ) throws IOException {
            boolean visible = isVisible(MediaType.valueOf(contentType == null ? "application/json" : contentType));
            if (visible) {
                byte[] content = StreamUtils.copyToByteArray(inputStream);
                if (content.length > 0) {
                    String contentString = new String(content);
                    log.info("{} Payload: {}", direction, contentString);
                }
            } else {
                log.info("{} Payload: Binary Content", direction);
            }
        }
    
        private static boolean isVisible(MediaType mediaType) {
            final List<MediaType> VISIBLE_TYPES = Arrays.asList(MediaType.valueOf("text/*"),
                MediaType.APPLICATION_FORM_URLENCODED,
                MediaType.APPLICATION_JSON,
                MediaType.APPLICATION_XML,
                MediaType.valueOf("application/*+json"),
                MediaType.valueOf("application/*+xml"),
                MediaType.MULTIPART_FORM_DATA);
            return VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
        }
    
        private boolean isMultipartRequest(HttpServletRequest request) {
            String contentType = request.getContentType();
            return request.getMethod().equalsIgnoreCase("POST")
                && contentType != null
                && contentType.startsWith("multipart/form-data");
        }
    }
    

    CachedHttpServletRequestWrapper.java

    public class CachedHttpServletRequestWrapper extends HttpServletRequestWrapper {
    
        private final byte[] cachedBody;
        private Map<String, String[]> parameterMap;
    
        public CachedHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            this.cachedBody = request.getInputStream().readAllBytes();
    
            // form-urlencoded νƒ€μž…μΌ λ•Œλ§Œ νŒŒλΌλ―Έν„° νŒŒμ‹±
            String contentType = request.getContentType();
            if (contentType != null && contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
                String encoding = request.getCharacterEncoding();
                if (encoding == null) {
                    encoding = StandardCharsets.UTF_8.name();
                }
                this.parameterMap = parseFormParameters(new String(this.cachedBody, encoding));
            } else {
                // 원본 request의 νŒŒλΌλ―Έν„° λ§΅ μ‚¬μš©
                this.parameterMap = super.getParameterMap();
            }
        }
    
        @Override
        public ServletInputStream getInputStream() {
            ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
    
            return new ServletInputStream() {
                @Override
                public int read() {
                    return bais.read();
                }
    
                @Override
                public boolean isFinished() {
                    return bais.available() == 0;
                }
    
                @Override
                public boolean isReady() {
                    return true;
                }
    
                @Override
                public void setReadListener(ReadListener listener) {
                }
            };
        }
    
        @Override
        public Map<String, String[]> getParameterMap() {
            return this.parameterMap;
        }
    
        @Override
        public BufferedReader getReader() {
            String encoding = getCharacterEncoding();
            if (encoding == null) {
                encoding = StandardCharsets.UTF_8.name();
            }
            return new BufferedReader(new InputStreamReader(getInputStream(), Charset.forName(encoding)));
        }
    
        private Map<String, String[]> parseFormParameters(String body) throws UnsupportedEncodingException {
            Map<String, List<String>> tempMap = new LinkedHashMap<>();
    
            // 빈 λ°”λ”” 처리
            if (body == null || body.trim().isEmpty()) {
                return new LinkedHashMap<>();
            }
    
            for (String pair : body.split("&")) {
                if (pair.trim().isEmpty()) {
                    continue; // 빈 pair κ±΄λ„ˆλ›°κΈ°
                }
    
                String[] parts = pair.split("=", 2);
                if (parts.length == 0) {
                    continue;
                }
    
                String key = URLDecoder.decode(parts[0], StandardCharsets.UTF_8);
                String value = parts.length > 1 ? URLDecoder.decode(parts[1], StandardCharsets.UTF_8) : "";
                tempMap.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
            }
    
            return tempMap.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toArray(new String[0])));
        }
    }
    

    CachedHttpServletResponseWrapper.java

    public class CachedHttpServletResponseWrapper extends ContentCachingResponseWrapper {
    
        public CachedHttpServletResponseWrapper(HttpServletResponse response) {
            super(response);
        }
    }
    
    Created Mon, 16 Jun 2025 20:49:58 +0900