Avatar
πŸ˜‰

Organizations

  • 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
  • μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μž…μž₯μ—μ„œ μƒ€λ”©λœ DBλ₯Ό μ“°κΈ° μœ„ν•΄μ„œ 직접 β€œλ³΅μž‘ν•œβ€ μ½”λ“œλ₯Ό 많이 μ§€ ν•„μš”λŠ” μ—†μŠ΅λ‹ˆλ‹€. λŒ€μ‹ , 주둜 λ‹€μŒ 두 κ°€μ§€ 레벨 쀑 ν•˜λ‚˜μ˜ λ°©μ‹μœΌλ‘œ 샀딩을 μ²˜λ¦¬ν•˜κ²Œ λ©λ‹ˆλ‹€.


    1. μ™ΈλΆ€ ν”„λ‘μ‹œ/미듀웨어 μ‚¬μš© (Mycat, Vitess, ProxySQL λ“±)

    • μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ³€κ²½ 제둜 λ˜λŠ” μ΅œμ†Œν™”

      • μƒ€λ“œλ³„ DB URL(호슀트/포트) λŒ€μ‹ , ν”„λ‘μ‹œμ˜ 단일 접점(가상 호슀트/포트)을 spring.datasource.url에 μ§€μ •
      • νŠΈλž˜ν”½Β·μƒ€λ“œ ν‚€ 기반 λΆ„κΈ° λ‘œμ§μ€ ν”„λ‘μ‹œκ°€ λŒ€μ‹  μˆ˜ν–‰
    • κ΅¬ν˜„ 사항

      1. application.yml에 ν•œλ²ˆλ§Œ ν”„λ‘μ‹œ 접점 정보 등둝

        spring:
          datasource:
            url: jdbc:mysql://proxy.example.com:3306/app_db
            username: app
            password: secret
        
      2. ν•„μš” μ‹œ νŠΈλžœμž­μ…˜ 격리, 컀λ„₯μ…˜ ν’€, λ¦¬ν”Œλ¦¬μΉ΄ 읽기 μ „μš© μ„€μ • λ“± μΆ”κ°€ ꡬ성

    Created Tue, 10 Jun 2025 08:49:58 +0900
  • λ°μ΄ν„°λ² μ΄μŠ€(DB) 샀딩(DB Sharding)μ΄λž€ λŒ€μš©λŸ‰ 데이터λ₯Ό 효율적으둜 λΆ„μ‚°Β·κ΄€λ¦¬ν•˜κΈ° μœ„ν•΄ ν•˜λ‚˜μ˜ κ±°λŒ€ν•œ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μ—¬λŸ¬ 개의 μž‘μ€ 쑰각(μƒ€λ“œ, shard)으둜 λ‚˜λˆ„μ–΄ 각각을 λ³„λ„μ˜ μ„œλ²„μ— λΆ„μ‚° μ €μž₯ν•˜κ³  μ²˜λ¦¬ν•˜λŠ” 기법을 λ§ν•©λ‹ˆλ‹€.


    1. μ™œ 샀딩이 ν•„μš”ν•œκ°€?

    • μ„±λŠ₯ ν™•μž₯μ„±(Scalability)

      • 단일 μ„œλ²„μ— λͺ¨λ“  데이터λ₯Ό μ €μž₯ν•˜λ©΄ μ €μž₯ μš©λŸ‰Β·μ²˜λ¦¬ λŠ₯λ ₯이 ν•œκ³„μ— λΆ€λ”ͺνž™λ‹ˆλ‹€.
      • 데이터와 νŠΈλž˜ν”½μ΄ λŠ˜μ–΄λ‚ μˆ˜λ‘ λ‘œλ“œκ°€ μ§‘μ€‘λ˜μ–΄ 응닡 μ§€μ—°(latency)μ΄λ‚˜ 병λͺ©(bottleneck)이 λ°œμƒν•©λ‹ˆλ‹€.
    • κ³ κ°€μš©μ„±(High Availability)

      • 각 μƒ€λ“œλ₯Ό λ…λ¦½λœ λ…Έλ“œμ— 두면, 일뢀 λ…Έλ“œ μž₯μ•  μ‹œμ—λ„ 전체 μ‹œμŠ€ν…œμ΄ μ™„μ „νžˆ λ‹€μš΄λ˜μ§€ μ•Šκ³  κ°€μš©μ„±μ„ μœ μ§€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
    • 관리 μš©μ΄μ„±

    Created Tue, 10 Jun 2025 08:44:58 +0900
  • λ°°κ²½

    • local, dev, prod λ“± ν”„λ‘œνŒŒμΌμ—μ„  mysql μ…‹νŒ…
    • test ν”„λ‘œνŒŒμΌμ€ h2에 schema.sql, data.sql둜 데이터 μ…‹νŒ…
    • nativeQueryλ₯Ό μ‚¬μš©ν•˜λŠ” 경우 DATE_FORMAT 같은 mysql ν•¨μˆ˜ μ‚¬μš©.
    spring:
      datasource:
        url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1
    

    DATE_FORMAT λŒ€μ²΄

    public class H2Functions {
        public static String formatDate(java.sql.Timestamp timestamp, String format) {
            return new java.text.SimpleDateFormat(format).format(timestamp);
        }
    }
    

    schema.sql

    CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR "com.example.H2Functions.formatDate";
    

    Repository

    @Query(value = "SELECT DATE_FORMAT(order_date, 'yyyy-MM-dd') AS formatted_date, COUNT(*) " +
                   "FROM orders GROUP BY formatted_date", nativeQuery = true)
    List<Object[]> findOrderCountGroupedByDate();
    

    μœ„μ˜ μ½”λ“œλŠ” μ˜ˆμ‹œμ΄λ©°, μ‹€μ œλ‘œλŠ” h2와 mysqlμ—μ„œ format으둜 λ„˜κ²¨μ£ΌλŠ” ν˜•νƒœκ°€ λ‹€λ¦…λ‹ˆλ‹€. mysqlμ—μ„œλŠ” %m%d, %Y λ“±μœΌλ‘œ μ“°κ³  h2에선 ‘MMdd’, ‘yyyy’ 등을 μ‚¬μš©ν•©λ‹ˆλ‹€.

    Created Sun, 01 Jun 2025 20:20:45 +0900