Avatar
๐Ÿ˜‰

Organizations

  • jwt ๊ด€๋ จ Tip

    Q. access token ์ด ๋งŒ๋ฃŒ๋˜๋ฉด, refresh token์„ ํ†ตํ•ด ์žฌ๋ฐœํ–‰ ํ•œ๋‹ค. refresh token์€ ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์ด ์—†์„๊นŒ?

    A. Refresh Token๋„ ๋ฐ˜๋“œ์‹œ ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    • ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” Access Token๋ณด๋‹ค ํ›จ์”ฌ ๊ธด ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•  ๋ฟ์ด์—์š”.

    1. ์ผ๋ฐ˜์ ์ธ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •

    • Access Token: ๋ณดํ†ต 15๋ถ„ ~ 1์‹œ๊ฐ„
    • Refresh Token: ๋ณดํ†ต 7์ผ ~ 30์ผ ๋˜๋Š” 90์ผ

    2. ์™œ Refresh Token๋„ ๋งŒ๋ฃŒ ๊ธฐ๊ฐ„์ด ํ•„์š”ํ•œ๊ฐ€?

    • Refresh Token์ด ์œ ์ถœ๋˜์—ˆ์„ ๊ฒฝ์šฐ, ๋ฌด์ œํ•œ์œผ๋กœ Access Token์„ ์žฌ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค๋ฉด ๋ณด์•ˆ์— ์น˜๋ช…์ ์ž…๋‹ˆ๋‹ค.
    • ์ผ์ • ๊ธฐ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๋กœ๊ทธ์ธ ์„ธ์…˜์„ ์žฌํ™•์ธํ•˜๊ฑฐ๋‚˜ ์žฌ๋กœ๊ทธ์ธ์„ ์œ ๋„ํ•˜๋Š” ๊ฒƒ์ด ๋ณด์•ˆ์ƒ ๋” ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

    3. ์ €์žฅ ์œ„์น˜๋Š”?

    • Refresh Token์€ ๋ณดํ†ต ์„œ๋ฒ„ ์ €์žฅ ๋ฐฉ์‹ (e.g., Redis)์— ์ €์žฅํ•˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ์‹๋ณ„์ž๋งŒ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜ ํด๋ผ์ด์–ธํŠธ์— ์ €์žฅํ•˜๋˜ ๋ณด์•ˆ์ ์œผ๋กœ ์•ˆ์ „ํ•œ ์ €์žฅ์†Œ (e.g., Secure Cookie, LocalStorage X) ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค.

    Q. Spring Boot์—์„œ์˜ ์˜ˆ์‹œ

    A. Spring Boot์—์„œ JWT ๊ธฐ๋ฐ˜์œผ๋กœ Access Token๊ณผ Refresh Token์„ ๋ฐœ๊ธ‰ ๋ฐ ์žฌ๋ฐœ๊ธ‰ํ•˜๋Š” ๊ธฐ๋ณธ์ ์ธ ๊ตฌ์กฐ๋ฅผ ์˜ˆ์‹œ


    1. ํ† ํฐ ์ƒ์„ฑ ํด๋ž˜์Šค (JwtProvider)

    @Component
    public class JwtProvider {
    
        private final String secretKey = "yourSecretKey"; // ์ ์ ˆํžˆ ์•”ํ˜ธํ™” ํ•„์š”
        private final long accessTokenValidity = 1000L * 60 * 15;  // 15๋ถ„
        private final long refreshTokenValidity = 1000L * 60 * 60 * 24 * 14; // 14์ผ
    
        public String createAccessToken(String userId) {
            return createToken(userId, accessTokenValidity);
        }
    
        public String createRefreshToken(String userId) {
            return createToken(userId, refreshTokenValidity);
        }
    
        private String createToken(String userId, long validity) {
            Claims claims = Jwts.claims().setSubject(userId);
            Date now = new Date();
            Date expiry = new Date(now.getTime() + validity);
    
            return Jwts.builder()
                    .setClaims(claims)
                    .setIssuedAt(now)
                    .setExpiration(expiry)
                    .signWith(SignatureAlgorithm.HS256, secretKey)
                    .compact();
        }
    
        public String getUserIdFromToken(String token) {
            return Jwts.parser().setSigningKey(secretKey)
                    .parseClaimsJws(token).getBody().getSubject();
        }
    
        public boolean validateToken(String token) {
            try {
                Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
                return true;
            } catch (ExpiredJwtException e) {
                return false;
            }
        }
    }
    

    2. ๋กœ๊ทธ์ธ ์‹œ ํ† ํฐ ๋ฐœ๊ธ‰

    @PostMapping("/login")
    public ResponseEntity<TokenResponse> login(@RequestBody LoginRequest request) {
        // ์ธ์ฆ ์ฒ˜๋ฆฌ (์ƒ๋žต)
        String userId = "user-id";
    
        String accessToken = jwtProvider.createAccessToken(userId);
        String refreshToken = jwtProvider.createRefreshToken(userId);
    
        // Refresh Token์„ DB๋‚˜ Redis์— ์ €์žฅ
        refreshTokenService.save(userId, refreshToken);
    
        return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken));
    }
    

    3. Access Token ๋งŒ๋ฃŒ ์‹œ Refresh Token์œผ๋กœ ์žฌ๋ฐœ๊ธ‰

    @PostMapping("/token/refresh")
    public ResponseEntity<TokenResponse> refreshToken(@RequestBody TokenRequest request) {
        String refreshToken = request.getRefreshToken();
    
        if (!jwtProvider.validateToken(refreshToken)) {
            throw new RuntimeException("Invalid refresh token");
        }
    
        String userId = jwtProvider.getUserIdFromToken(refreshToken);
    
        // ์ €์žฅ๋œ refresh token๊ณผ ๋น„๊ต
        String stored = refreshTokenService.get(userId);
        if (!stored.equals(refreshToken)) {
            throw new RuntimeException("Token mismatch");
        }
    
        String newAccessToken = jwtProvider.createAccessToken(userId);
        return ResponseEntity.ok(new TokenResponse(newAccessToken, refreshToken));
    }
    

    4. Refresh Token ์ €์žฅ ์˜ˆ์‹œ (Redis ๊ธฐ๋ฐ˜)

    @Service
    public class RefreshTokenService {
    
        private final RedisTemplate<String, String> redisTemplate;
    
        public RefreshTokenService(RedisTemplate<String, String> redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        public void save(String userId, String token) {
            redisTemplate.opsForValue().set(userId, token, 14, TimeUnit.DAYS); // ์œ ํšจ ๊ธฐ๊ฐ„ ์„ค์ •
        }
    
        public String get(String userId) {
            return redisTemplate.opsForValue().get(userId);
        }
    }
    

    Q. ์—์‹œ์—์„œ RefreshToken ์—์„œ refresh save์ „์— ๊ธฐ์กด๊บผ๋Š” ์•ˆ์ง€์›Œ๋„ ๋ ๊นŒ?

    A. ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” ๊ธฐ์กด Refresh Token์„ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ๋ฎ์–ด์จ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์™œ๋ƒ๋ฉด:

    Created Thu, 27 Mar 2025 11:36:45 +0900
  • Spring Boot์—์„œ Kafka๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ฝ”๋“œ๋กœ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ด๋ณผ๊ฒŒ์š”.
    Kafka ํ”„๋กœ๋“€์„œ์™€ ์ปจ์Šˆ๋จธ์—์„œ Fallback ๋ฉ”์†Œ๋“œ, ์žฌ์‹œ๋„ (Retry), ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋“ฑ์„ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.


    1. Kafka ํ”„๋กœ๋“€์„œ์—์„œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ (Fallback, Retry)

    Kafka ํ”„๋กœ๋“€์„œ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ธŒ๋กœ์ปค๋กœ ๋ณด๋‚ด๋Š” ๊ณผ์ •์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    ์˜ˆ๋ฅผ ๋“ค์–ด, ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ, ๋ธŒ๋กœ์ปค ๋‹ค์šด, ๋ฉ”์‹œ์ง€ ํฌ๊ธฐ ์ดˆ๊ณผ ๋“ฑ์˜ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋Š”๋ฐ์š”, ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

    โœ… (1) Fallback ๋ฉ”์†Œ๋“œ ์ ์šฉ (Resilience4j ์‚ฌ์šฉ)

    Resilience4j์˜ @Retry์™€ @Recover๋ฅผ ํ™œ์šฉํ•˜๋ฉด
    Kafka ์ „์†ก์ด ์‹คํŒจํ–ˆ์„ ๋•Œ ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„๋ฅผ ํ•˜๊ณ , ์ตœ์ข…์ ์œผ๋กœ ์‹คํŒจํ•˜๋ฉด ๋Œ€์ฒด ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    Created Wed, 26 Mar 2025 13:38:03 +0900
  • Apache Kafka ์‚ฌ์šฉ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์š” ์˜ค๋ฅ˜ ์œ ํ˜•๊ณผ ๊ทธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


    1. ๋„คํŠธ์›Œํฌ ๋ฐ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜

    โŒ ์˜ค๋ฅ˜: Connection to broker <broker_id> failed

    ๐Ÿ“Œ ์›์ธ: ๋ธŒ๋กœ์ปค์™€์˜ ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ ๋ฌธ์ œ, ๋ธŒ๋กœ์ปค ๋‹ค์šด, ๋ฐฉํ™”๋ฒฝ ๋ฌธ์ œ ๋“ฑ
    โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

    • zookeeper์™€ broker๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ (ps aux | grep kafka)
    • listeners ๋ฐ advertised.listeners ์„ค์ • ํ™•์ธ (server.properties)
    • ๋ฐฉํ™”๋ฒฝ ์„ค์ • (iptables -L, firewalld)์„ ์ ๊ฒ€ํ•˜์—ฌ ํฌํŠธ๊ฐ€ ์—ด๋ ค ์žˆ๋Š”์ง€ ํ™•์ธ

    2. ๋ธŒ๋กœ์ปค ๋ฐ ํ† ํ”ฝ ๊ด€๋ จ ์˜ค๋ฅ˜

    โŒ ์˜ค๋ฅ˜: Leader not available for partition <partition_id>

    ๐Ÿ“Œ ์›์ธ:

    Created Wed, 26 Mar 2025 13:30:03 +0900
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์— ๋Œ€ํ•ด Lock๊ณผ Unlock์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐ ํŠธ๋žœ์žญ์…˜์˜ ๋™์ž‘ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ ํ…Œ์ด๋ธ” ์ž ๊ธˆ, ํ–‰ ์ž ๊ธˆ, ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€, ๋ช…์‹œ์  ์ž ๊ธˆ ํ•ด์ œ๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Java ํ™˜๊ฒฝ์—์„œ๋Š” JDBC, JPA, QueryDSL ๋“ฑ์„ ํ™œ์šฉํ•ด ์ด๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์•„๋ž˜๋Š” SELECT๋ฟ๋งŒ ์•„๋‹ˆ๋ผ INSERT, UPDATE, DELETE ๋“ฑ ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์—์„œ Lock๊ณผ Unlock์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.


    1. ํ…Œ์ด๋ธ” ์ž ๊ธˆ (Table Lock)

    ํ…Œ์ด๋ธ” ๋‹จ์œ„๋กœ ์ž ๊ธˆ์„ ์„ค์ •ํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์ด ์™„๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์˜ ์ ‘๊ทผ์„ ๋ง‰์Šต๋‹ˆ๋‹ค.

    Created Wed, 26 Mar 2025 11:33:45 +0900
  • QueryDSL์„ ์‚ฌ์šฉํ•  ๋•Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ Lock ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ JPA์™€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. QueryDSL์€ JPA์™€ ํ†ตํ•ฉ๋˜์–ด ๋™์ž‘ํ•˜๋ฉฐ, JPQL ๋ฐ Native Query์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ž ๊ธˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” QueryDSL์—์„œ Lock๊ณผ ๊ด€๋ จ๋œ ์ฃผ์š” ๊ธฐ๋Šฅ๊ณผ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.


    1. QueryDSL์—์„œ Pessimistic Lock ์‚ฌ์šฉ

    QueryDSL์€ JPA LockModeType์„ ์ง€์›ํ•˜์—ฌ ๋น„๊ด€์  ์ž ๊ธˆ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์˜ˆ์ œ: LockModeType.PESSIMISTIC_WRITE ์‚ฌ์šฉ

    import com.querydsl.jpa.impl.JPAQueryFactory;
    import jakarta.persistence.EntityManager;
    import jakarta.persistence.LockModeType;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class ProductService {
        private final JPAQueryFactory queryFactory;
    
        public ProductService(EntityManager entityManager) {
            this.queryFactory = new JPAQueryFactory(entityManager);
        }
    
        @Transactional
        public Product findByIdWithLock(Long productId) {
            QProduct product = QProduct.product;
    
            // Pessimistic Write Lock ์„ค์ •
            return queryFactory.selectFrom(product)
                    .where(product.id.eq(productId))
                    .setLockMode(LockModeType.PESSIMISTIC_WRITE)
                    .fetchOne();
        }
    }
    

    ์ฃผ์š” ํฌ์ธํŠธ:

    • setLockMode(LockModeType.PESSIMISTIC_WRITE)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋น„๊ด€์  ์“ฐ๊ธฐ ์ž ๊ธˆ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • PESSIMISTIC_READ์™€ ๊ฐ™์€ ๋‹ค๋ฅธ ์ž ๊ธˆ ๋ชจ๋“œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    2. ๋‚™๊ด€์  ์ž ๊ธˆ (Optimistic Lock)

    ๋‚™๊ด€์  ์ž ๊ธˆ์€ QueryDSL์—์„œ ๋ณ„๋„๋กœ ์ง€์›๋˜์ง€ ์•Š์ง€๋งŒ, JPA ์—”ํ„ฐํ‹ฐ์˜ @Version ํ•„๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

    Created Wed, 26 Mar 2025 11:30:45 +0900
  • Java ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•  ๋•Œ -Xmx512m๊ณผ -XX:MaxRAM=512m๋Š” ๋ชจ๋‘ ์ตœ๋Œ€ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ œํ•œํ•˜๋Š” ์˜ต์…˜์ด์ง€๋งŒ, ๋™์ž‘ ๋ฐฉ์‹์— ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

    1. -Xmx512m

    • ํž™(Heap) ๋ฉ”๋ชจ๋ฆฌ์˜ ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ 512MB๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • ์˜ˆ์ œ:
      java -Xmx512m -jar myapp.jar
      
    • ์ด ์„ค์ •์€ Java ํž™ ๋ฉ”๋ชจ๋ฆฌ๋งŒ ์ œํ•œํ•˜๋ฉฐ, ์ „์ฒด ํ”„๋กœ์„ธ์Šค์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ œํ•œํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค.
    • ์ฆ‰, ํž™ ์™ธ์˜ ๋ฉ”๋ชจ๋ฆฌ(๋ฉ”ํƒ€์ŠคํŽ˜์ด์Šค, ์Šคํƒ, ๋„ค์ดํ‹ฐ๋ธŒ ๋ฉ”๋ชจ๋ฆฌ ๋“ฑ)๋Š” ๋”ฐ๋กœ ์ œํ•œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    2. -XX:MaxRAM=512m

    • JVM์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ „์ฒด ๋ฉ”๋ชจ๋ฆฌ(ํž™ + ๋ฉ”ํƒ€์ŠคํŽ˜์ด์Šค + ๊ธฐํƒ€ ๋„ค์ดํ‹ฐ๋ธŒ ๋ฉ”๋ชจ๋ฆฌ) ๋ฅผ 512MB๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.
    • ์˜ˆ์ œ:
      java -XX:MaxRAM=512m -jar myapp.jar
      
    • -XX:MaxRAM์„ ์„ค์ •ํ•˜๋ฉด -Xmx์˜ ๊ธฐ๋ณธ๊ฐ’์ด ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      • ์ผ๋ฐ˜์ ์œผ๋กœ -Xmx์˜ ๊ธฐ๋ณธ๊ฐ’์€ MaxRAM ๊ฐ’์˜ 25~50% ์ •๋„๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค(JVM ๋ฒ„์ „์— ๋”ฐ๋ผ ๋‹ค๋ฆ„).
    • ์ด ์„ค์ •์€ ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ(Docker, Kubernetes) ์—์„œ JVM์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

    ์ฐจ์ด์  ์š”์•ฝ

    ์˜ต์…˜ ๋Œ€์ƒ ์„ค๋ช…
    -Xmx512m ํž™(Heap) ๋ฉ”๋ชจ๋ฆฌ ํž™ ๋ฉ”๋ชจ๋ฆฌ์˜ ์ตœ๋Œ€ ํฌ๊ธฐ๋งŒ ์ œํ•œ
    -XX:MaxRAM=512m ์ „์ฒด JVM ๋ฉ”๋ชจ๋ฆฌ ํž™ + ๋ฉ”ํƒ€์ŠคํŽ˜์ด์Šค + ๋„ค์ดํ‹ฐ๋ธŒ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํฌํ•จํ•œ JVM ์ „์ฒด ๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ

    ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด?

    ๋‘ ์˜ต์…˜์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

    Created Wed, 26 Mar 2025 11:26:45 +0900