-
JavaScriptμμ **ν΄λ‘μ (Closure)**λ ν¨μμ κ·Έ ν¨μκ° μ μΈλ μ΄νμ νκ²½(Lexical Environment)μ μ‘°ν©μ λ§ν©λλ€. μ¦, ν¨μκ° μΈλΆ μ€μ½νμ λ³μμ μ κ·Όν μ μλ κΈ°λ₯μ μλ―Έν©λλ€.
π κΈ°λ³Έ κ°λ
ν΄λ‘μ λ λ€μκ³Ό κ°μ 쑰건μμ μμ±λ©λλ€:
- ν¨μ μμ ν¨μκ° μ μλμ΄ μκ³ ,
- λ΄λΆ ν¨μκ° μΈλΆ ν¨μμ λ³μμ μ κ·Όν μ μμΌλ©°,
- μΈλΆ ν¨μμ μ€νμ΄ λλ λ€μλ λ΄λΆ ν¨μκ° μ¬μ ν κ·Έ λ³μμ μ κ·Όν μ μμ λ ν΄λ‘μ κ° λ©λλ€.
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μ κ³μ μ κ·Όν μ μλ μ΄μ λ ν΄λ‘μ λλ¬Έμ λλ€.CreatedFri, 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λ‘ μ±μμ§λλ€.CreatedWed, 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); } }CreatedMon, 16 Jun 2025 20:49:58 +0900 -
μ€νλ§ λΆνΈ μ ν리μΌμ΄μ μ μ₯μμ μ€λ©λ DBλ₯Ό μ°κΈ° μν΄μ μ§μ β볡μ‘νβ μ½λλ₯Ό λ§μ΄ μ§€ νμλ μμ΅λλ€. λμ , μ£Όλ‘ λ€μ λ κ°μ§ λ 벨 μ€ νλμ λ°©μμΌλ‘ μ€λ©μ μ²λ¦¬νκ² λ©λλ€.
1. μΈλΆ νλ‘μ/λ―Έλ€μ¨μ΄ μ¬μ© (Mycat, Vitess, ProxySQL λ±)
-
μ ν리μΌμ΄μ λ³κ²½ μ λ‘ λλ μ΅μν
- μ€λλ³ DB URL(νΈμ€νΈ/ν¬νΈ) λμ , νλ‘μμ λ¨μΌ μ μ (κ°μ νΈμ€νΈ/ν¬νΈ)μ
spring.datasource.urlμ μ§μ - νΈλν½Β·μ€λ ν€ κΈ°λ° λΆκΈ° λ‘μ§μ νλ‘μκ° λμ μν
- μ€λλ³ DB URL(νΈμ€νΈ/ν¬νΈ) λμ , νλ‘μμ λ¨μΌ μ μ (κ°μ νΈμ€νΈ/ν¬νΈ)μ
-
ꡬν μ¬ν
-
application.ymlμ νλ²λ§ νλ‘μ μ μ μ 보 λ±λ‘spring: datasource: url: jdbc:mysql://proxy.example.com:3306/app_db username: app password: secret -
νμ μ νΈλμμ 격리, 컀λ₯μ ν, 리ν리카 μ½κΈ° μ μ© μ€μ λ± μΆκ° ꡬμ±
-
CreatedTue, 10 Jun 2025 08:49:58 +0900 -
-
λ°μ΄ν°λ² μ΄μ€(DB) μ€λ©(DB Sharding)μ΄λ λμ©λ λ°μ΄ν°λ₯Ό ν¨μ¨μ μΌλ‘ λΆμ°Β·κ΄λ¦¬νκΈ° μν΄ νλμ κ±°λν λ°μ΄ν°λ² μ΄μ€λ₯Ό μ¬λ¬ κ°μ μμ μ‘°κ°(μ€λ, shard)μΌλ‘ λλμ΄ κ°κ°μ λ³λμ μλ²μ λΆμ° μ μ₯νκ³ μ²λ¦¬νλ κΈ°λ²μ λ§ν©λλ€.
1. μ μ€λ©μ΄ νμνκ°?
-
μ±λ₯ νμ₯μ±(Scalability)
- λ¨μΌ μλ²μ λͺ¨λ λ°μ΄ν°λ₯Ό μ μ₯νλ©΄ μ μ₯ μ©λΒ·μ²λ¦¬ λ₯λ ₯μ΄ νκ³μ λΆλͺνλλ€.
- λ°μ΄ν°μ νΈλν½μ΄ λμ΄λ μλ‘ λ‘λκ° μ§μ€λμ΄ μλ΅ μ§μ°(latency)μ΄λ λ³λͺ©(bottleneck)μ΄ λ°μν©λλ€.
-
κ³ κ°μ©μ±(High Availability)
- κ° μ€λλ₯Ό λ 립λ λ Έλμ λλ©΄, μΌλΆ λ Έλ μ₯μ μμλ μ 체 μμ€ν μ΄ μμ ν λ€μ΄λμ§ μκ³ κ°μ©μ±μ μ μ§ν μ μμ΅λλ€.
-
κ΄λ¦¬ μ©μ΄μ±
CreatedTue, 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=-1DATE_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’ λ±μ μ¬μ©ν©λλ€.
CreatedSun, 01 Jun 2025 20:20:45 +0900