๊ด€๋ฆฌ ๋ฉ”๋‰ด

FS_hansol

[Spring] newfeedproject_Trouble Shooting(auth) ๋ณธ๋ฌธ

Spring_๋ถ€์บ /Trouble Tang Tang ๐Ÿ”ซ๐Ÿ”ซ

[Spring] newfeedproject_Trouble Shooting(auth)

FS29 2025. 8. 9. 21:36

1) ์ธ์ฆ ๊ฐ์ฒด ๋“ฑ๋ก์ด ๋’ค์„ž์ž„

์›๋ž˜ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ๋˜ ์˜๋„

JwtAuthorizationFilter ์˜ ๋ชฉ์ ์€ 

  1. ์š”์ฒญ์˜ Authorization ํ—ค๋”์—์„œ JWT ํ† ํฐ์„ ๊บผ๋‚ด์˜จ๋‹ค.
  2. ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์ง€ ์•Š๊ณ , ์œ„์กฐ๋˜์ง€ ์•Š์€์ง€ ํ™•์ธํ•œ๋‹ค.
  3. ํ† ํฐ ์•ˆ์— ๋“ค์–ด์žˆ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด(์ด๋ฉ”์ผ)๋ฅผ ๊บผ๋‚ด์„œ DB์—์„œ ์ง„์งœ ์œ ์ € ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  4. ๊ทธ ์œ ์ € ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Spring Security ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ ๋‹ค.
  5. ๋งŒ๋“  ์ธ์ฆ ๊ฐ์ฒด๋ฅผ SecurityContext์— ๋„ฃ์–ด์„œ, ์ดํ›„ ์š”์ฒญ ์ฒ˜๋ฆฌ ๊ณผ์ •์—์„œ "์•„~!~! ์ด ์š”์ฒญ์€ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์š”์ฒญ์ด๊ตฌ๋‚˜" ํ•˜๊ณ  ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.

์ฆ‰, JWT ํ† ํฐ์œผ๋กœ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์ž„์„ Spring Security์— ์•Œ๋ ค์ฃผ๋Š” ๋‹ค๋ฆฌ ์—ญํ• 

 

๋ฌธ์ œ๊ฐ€ ๋œ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain)
        throws ServletException, IOException {
    // Authorization ํ—ค๋”์—์„œ Bearer ํ† ํฐ์„ ์ถ”์ถœ
    String header = request.getHeader("Authorization");
    if (header != null && header.startsWith("Bearer ")) {
        String token = header.substring(7);  // "Bearer " ์ดํ›„์˜ ํ† ํฐ ๋ถ€๋ถ„๋งŒ ์ถ”์ถœ

        // ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
        if (JwtUtil.validateToken(token) && !"refresh".equals(JwtUtil.getUserEmailFromToken(token))) {
            // JWT์—์„œ ์ด๋ฉ”์ผ์„ ์ถ”์ถœ
            String email = JwtUtil.getUserEmailFromToken(token);

            // DB์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋กœ๋“œ
            UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(email);
            // UserDetails๋ฅผ UserDetailsImpl๋กœ ์บ์ŠคํŒ…
            UserDetailsImpl userDetailsImpl = (UserDetailsImpl) userDetails;  

            // ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
            Authentication auth = new UsernamePasswordAuthenticationToken(
            // ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
                    userDetailsImpl, null, userDetailsImpl.getAuthorities()); 

            // ์ธ์ฆ ์ •๋ณด๋ฅผ SecurityContext์— ์„ค์ •
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
    }

    // ํ•„ํ„ฐ ์ฒด์ธ์—์„œ ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ๋„˜์–ด๊ฐ
    filterChain.doFilter(request, response);
}

 

์›์ธ ๋ถ„์„

 

 

  • ์ค‘๋ณต ๋กœ์ง ๋ฐœ์ƒ
    ํ† ํฐ → ์ด๋ฉ”์ผ ์ถ”์ถœ → DB ์กฐํšŒ → ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ ๊ณผ์ •์ด ํ•„ํ„ฐ ๋‚ด๋ถ€์—์„œ ์ง์ ‘ ๊ตฌํ˜„๋จ
    → ๋‹ค๋ฅธ ํ•„ํ„ฐ/์„œ๋น„์Šค์—์„œ ๋™์ผ ๊ณผ์ •์ด ํ•„์š”ํ•˜๋ฉด ๋˜ ์ž‘์„ฑํ•ด์•ผ ํ•จ
  • ์œ ์ง€๋ณด์ˆ˜ ์–ด๋ ค์›€
    ์ธ์ฆ ๋กœ์ง์„ ์ˆ˜์ •ํ•  ๋•Œ ๋ชจ๋“  ํ•„ํ„ฐ ์ฝ”๋“œ๋ฅผ ์ฐพ์•„์„œ ์ˆ˜์ •ํ•ด์•ผ ํ•จ
    → ๋ฒ„๊ทธ ๊ฐ€๋Šฅ์„ฑ ์ฆ๊ฐ€
  • ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ์ €ํ•˜
    ํ•„ํ„ฐ์˜ ํ•ต์‹ฌ ์—ญํ• ์ด ๋ฌปํž˜ (ํ† ํฐ ๊ฒ€์ฆ + ์ธ์ฆ ๊ฐ์ฒด ๋“ฑ๋ก์ด ๋’ค์„ž์ž„)

 

    Spring Security Authentication ๊ฐ์ฒด ์ƒ์„ฑ(๊ถŒํ•œ์€ ๋นˆ ๋ฆฌ์ŠคํŠธ)
    public static Authentication getAuthentication(String token) {
        String email = getUserEmailFromToken(token);
        return new UsernamePasswordAuthenticationToken(email, null, List.of());
    }
ํ•ด๊ฒฐ๋œ ์ 

 

  • JwtUtil์— getAuthentication() ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€
    → ํ† ํฐ์—์„œ ์ด๋ฉ”์ผ ์ถ”์ถœ → DB ์กฐํšŒ → ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ๊นŒ์ง€ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ

 

๊ฐœ์„ ๋œ ์ฝ”๋“œ

  • UserDetailsImpl.java
 @Override
    public String getUsername() {
        // ์ด๋ฉ”์ผ์„ ๋กœ๊ทธ์ธ ์‹๋ณ„์ž๋กœ ์“ด๋‹ค๋ฉด
        return user.getUserName();
    }
// getEmail์„ ๋”ฐ๋กœ ์ƒ์„ฑํ•จ
    public String getEmail() {
        return user.getEmail();
    }

 

  • JwtAuthorizationFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain)
        throws ServletException, IOException {
    String header = request.getHeader("Authorization");
    if (header != null && header.startsWith("Bearer ")) {
        String token = header.substring(7);

        if (JwtUtil.validateToken(token) && !"refresh".equals(JwtUtil.getUserEmailFromToken(token))) {
            // ํ•ต์‹ฌ ๋ณ€๊ฒฝ: ์—ฌ๊ธฐ์„œ ์ง์ ‘ ์ธ์ฆ ๊ฐ์ฒด ์•ˆ ๋งŒ๋“ค๊ณ , JwtUtil ๋ฉ”์„œ๋“œ๋กœ ๋Œ€์ฒด
            Authentication auth = JwtUtil.getAuthentication(token, userDetailsServiceImpl);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
    }
    filterChain.doFilter(request, response);
}

์—ฌ๊ธฐ์„œ ์œ„ ์ฝ”๋“œ์˜

 if (JwtUtil.validateToken(token) && !"refresh".equals(JwtUtil.getUserEmailFromToken(token))) {

๋ฅผ ํ•œ ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค

public static String createAccessToken(Long userId, String email) {
    return JWT.create()
            .withSubject(email) // sub = ์‚ฌ์šฉ์ž email
            .withClaim("userId", userId)
            .withExpiresAt(new Date(System.currentTimeMillis() + ACCESS_EXP))
            .sign(algorithm);
}

Access ํ† ํฐ์€ subject์— ์‚ฌ์šฉ์ž์˜ ์ด๋ฉ”์ผ์„ ๋„ฃ๋Š”

public static String createRefreshToken(Long userId) {
    return JWT.create()
            .withSubject("refresh") // sub = "refresh" (๊ณ ์ • ๋ฌธ์ž์—ด)
            .withClaim("userId", userId)
            .withExpiresAt(new Date(System.currentTimeMillis() + REFRESH_EXP))
            .sign(algorithm);
}

Refresh ํ† ํฐ์€ subject์— "refresh"๋ผ๋Š” ๊ณ ์ • ๋ฌธ์ž์—ด์„ ๋„ฃ๋Š”๋‹ค

 

์ฆ‰, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์€ ์žฌ๋ฐœ๊ธ‰ ์šฉ๋„์ง€ ๋กœ๊ทธ์ธ ์ธ์ฆ์ธ๊ฐ€์— ์“ฐ๋ฉด ์•ˆ๋˜๊ธฐ ๋•Œ๋ฌธ์—, subject๊ฐ’์ด "refresh"์ธ ํ† ํฐ(์ฆ‰, ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ)์€ ํ•„ํ„ฐ์—์„œ ๊ฑธ๋Ÿฌ๋ฒ„๋ฆฌ๋Š” ๊ฑฐ์ž„

 

  • JwtUtil.java
public static Authentication getAuthentication(String token, UserDetailsServiceImpl userDetailsServiceImpl) {
    // 1. ํ† ํฐ์—์„œ ์ด๋ฉ”์ผ ๊บผ๋‚ด๊ธฐ
    String email = getUserEmailFromToken(token);
    
    // 2. ์ด๋ฉ”์ผ๋กœ DB์—์„œ ์œ ์ € ์ •๋ณด ๋กœ๋“œ
    UserDetailsImpl userDetails = userDetailsServiceImpl.loadUserByUsername(email);
    
    // 3. ์œ ์ € ์ •๋ณด ๊ธฐ๋ฐ˜์œผ๋กœ ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
    //    - principal: userDetails (์ธ์ฆ๋œ ์œ ์ € ์ •๋ณด)
    //    - credentials: null (๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„์š” ์—†์Œ)
    //    - authorities: List.of() (ํ˜„์žฌ ๊ถŒํ•œ ์—†์Œ)
    return new UsernamePasswordAuthenticationToken(userDetails, null, List.of());
}

 

 

  • ์ „ ์ฝ”๋“œ
    • ํ•„ํ„ฐ ๋‚ด๋ถ€์—์„œ ๋งค๋ฒˆ ํ† ํฐ ํŒŒ์‹ฑ → ์ด๋ฉ”์ผ ์ถ”์ถœ → DB ์กฐํšŒ → ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ → SecurityContextHolder ์„ธํŒ… => ์ค‘๋ณต + ์žฅํ™ฉ
  • ํ›„ ์ฝ”๋“œ
    • ํ•„ํ„ฐ์—์„œ๋Š” JwtUtil.getAuthentication() ํ•œ ์ค„๋กœ ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
      → ํ•„ํ„ฐ๊ฐ€ ๋” ์ฝ๊ธฐ ์‰ฝ๊ณ , ๋‹ค๋ฅธ ๊ณณ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ
      → ์œ ์ง€๋ณด์ˆ˜ ํŽธํ•จ

 

ํ•œ๋งˆ๋””๋กœ ํ•„ํ„ฐ์—์„œ ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ๋ณต์žกํ•œ ๊ณผ์ •์„ ์—†์• ๊ณ  

JwtUtil์ด ์•Œ์•„์„œ ๋‹ค ํ•ด์ฃผ๋„๋ก ์ˆ˜์ •ํ•จ

 

 


2) ํŒจ์Šค์›Œ๋“œ ๋ณ€๊ฒฝ Spring Security 403 Forbidden

 

์›๋ž˜ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ๋˜ ์˜๋„

๋ชฉํ‘œ๋กœ๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ํŠน์ • API(/api/myinfo/modify/password)์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ์„ค์ •
SecurityConfig์—์„œ URL ๊ถŒํ•œ ์„ค์ •์„ ํ–ˆ์Œ

 

JWT ์ธ์ฆํ•„ํ„ฐ๋ฅผ ํ†ตํ•ด SecurityContext์— ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ์„ธํŒ…ํ•˜๋Š” ๊ตฌ์กฐ

๋ฌธ์ œ๋ฐœ์ƒ

์ธ์ฆํ† ํฐ์„ ๋„ฃ๊ณ  ์š”์ฒญํ–ˆ์Œ์—๋„ HTTP 403 Forbidden์‘๋‹ต
ํฌ์ŠคํŠธ๋งจ์—์„œ๋Š” ์‘๋‹ต ์ฝ”๋“œ๋งŒ ๋ณด์ด๊ณ  ๊ตฌ์ฒด์ ์ธ ์›์ธ์„ ์•Œ ์ˆ˜ ์—†์—ˆ์Œ
์ฝ˜์†” ์ฐ์–ด๋ด๋„ JWT ํ† ํฐ๊ฐ’์ด ์ฐํžˆ๋Š”๋ฐ, ๋ฌธ์ œํŒŒ์•…์ด ์•ˆ๋˜์—ˆ์Œ

 

 

์›์ธ๋ถ„์„

application.properties์— 

logging.level.org.springframework.security=DEBUG

Spring Security ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ ํ๋ฆ„์ด ์ฝ˜์†”์— ์ƒ์„ธํžˆ ์ถœ๋ ฅ๋จ
ํ•„ํ„ฐ ์ฒด์ธ ์‹คํ–‰ ์ˆœ์„œ, ์ธ์ฆ/์ธ๊ฐ€ ํŒ๋‹จ๊ณผ์ • SecurityContext ์ƒํƒœ ๋“ฑ ํ™•์ธ ๊ฐ€๋Šฅ

DEBUG o.s.s.w.FilterChainProxy  : Secured POST /api/myinfo/modify/password
DEBUG o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous
DEBUG o.s.s.w.a.Http403ForbiddenEntryPoint    : Pre-authenticated entry point called. Rejecting access

 

์š”์ฒญ์ด Security FilterChain์— ์˜ํ•ด ๋ณด์•ˆ ๊ฒ€์‚ฌ๋ฅผ ๊ฑฐ์นจ
JwtAuthorizationFilter์—์„œ ์ธ์ฆ์ด ์„ธํŒ…๋˜์ง€ ์•Š์•„ AnonymousAuthenticationToken์œผ๋กœ ์ฒ˜๋ฆฌ ๋จ
์ธ๊ฐ€ ๋‹จ๊ณ„์—์„œ hasRole("USER")์กฐ๊ฑด ๋ถˆ์ถฉ์กฑ-> 403๋ฐœ์ƒ

 

ํ•ด๊ฒฐ๋ฐฉ๋ฒ• 

 

์ธ์ฆ ํ•„ํ„ฐ๊ฐ€ ํ•ด๋‹น ์š”์ฒญ์„ ์Šคํ‚ตํ•˜๊ณ  ์žˆ์—ˆ์Œ
SecurityConfig ๋˜๋Š” AuthorizationFilter์—์„œ /api/myinfo/modify/password๊ฒฝ๋กœ๋ฅผ ์ œ์™ธ ๋ชฉ๋ก์— ์ž˜๋ชป ํฌํ•จํ–ˆ๊ธฐ ๋•Œ๋ฌธ

  • ํ•„ํ„ฐ ์Šคํ‚ต์กฐ๊ฑด์—์„œ ํ•ด๋‹น๊ฒฝ๋กœ ์ œ๊ฑฐ
if ("POST".equalsIgnoreCase(request.getMethod())
    && "/api/auth/signin".equals(request.getRequestURI())) {
    filterChain.doFilter(request, response);
    return;
}
  •  SecurityConfig์— ๊ถŒํ•œ ์„ค์ • ๋ณ€๊ฒฝ
.requestMatchers("/api/myinfo/modify/password").hasRole("USER")

Spring Security๋ฌธ์ œ๋Š” ๋กœ๊ทธ ์—†์ด ์›์ธ ํŒŒ์•…์ด ์–ด๋ ต๋‹ค
logging.level.org.springframework.security=DEBUG๋Š” ํ•„์ˆ˜ ๋””๋ฒ„๊น… ๋„๊ตฌ
ํ•„ํ„ฐ ์Šคํ‚ต ์กฐ๊ฑด์€ ์ตœ์†Œํ•œ์œผ๋กœ ์œ ์ง€ํ•ด์•ผ ํ•˜๋ฉฐ ์ธ์ฆ์ด ํ•„์š”ํ•œ ๊ฒฝ๋กœ๋Š” ๋ฐ˜๋“œ์‹œ SecurityContext์— ์ธ์ฆ ๊ฐ์ฒด๊ฐ€ ๋“ค์–ด๊ฐ€์•ผ ํ•จ

 

 


3) ๋กœ๊ทธ์•„์›ƒ 403

 

์›๋ž˜ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ๋˜ ์˜๋„

 

์›๋ž˜ ์˜๋„๋Š” /api/auth/signout ์—”๋“œํฌ์ธํŠธ์—์„œ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž๋งŒ ๋กœ๊ทธ์•„์›ƒ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.
AccessToken์„ Authorization ํ—ค๋”์— ๋‹ด์•„ ์š”์ฒญ์„ ๋ณด๋ƒ„ JWT ํ•„ํ„ฐ์—์„œ ์ธ์ฆ ์„ธํŒ… hasRole("USER")์กฐ๊ฑด ํ†ต๊ณผํ›„ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ

 

 

๋ฌธ์ œ ๋ฐœ์ƒ

 

๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ ์‹œ ์„œ๋ฒ„ ์‘๋‹ต

{
    "timestamp": "2025-08-12T01:12:27.515+00:00",
    "status": 403,
    "error": "Forbidden",
    "message": "Forbidden",
    "path": "/api/auth/signout"
}

 

 

๋””๋ฒ„๊ทธ ๋กœ๊ทธ
[JWT] Authorization header = Bearer ...
AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
Http403ForbiddenEntryPoint     : Pre-authenticated entry point called. Rejecting access

ํ† ํฐ์ด ์žˆ๋Š”๋ฐ๋„ SecurityContext๊ฐ€ anonymous๋กœ ์„ธํŒ…๋˜๊ณ  hasRole("USER")๊ฒ€์‚ฌ์—์„œ 403๋ฐœ์ƒ

 

 

์›์ธ ๋ถ„์„

JWT์ธ์ฆ ํ•„ํ„ฐ์—์„œ /api/auth/signout์š”์ฒญ์„ ์Šคํ‚ตํ•˜๋„๋ก ์กฐ๊ฑด์ด ์„ค์ •๋˜์–ด ์žˆ์—ˆ์Œ

 

if ("POST".equalsIgnoreCase(request.getMethod())
    && ("/api/auth/signin".equals(request.getRequestURI())
    || "/api/auth/signout".equals(request.getRequestURI()))) {
    filterChain.doFilter(request, response);
    return;
}

 


์ด ์กฐ๊ฑด ๋•Œ๋ฌธ์— /api/auth/signout ์š”์ฒญ์€ JWT ์ธ์ฆ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์Œ
SecurityContext๋น„์–ด์žˆ์Œ
SecurityContext๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด Spring Security๋Š” AnonymousAuthenticationFilter๋กœ anonymous๋ฅผ ์„ธํŒ…ํ•จ
์ดํ›„ hasRole("USER")๊ฒ€์‚ฌ์—์„œ ์‹คํŒจ -> 403 Forbidden ๋ฐœ์ƒ

 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋กœ๊ทธ์•„์›ƒ์— ์ธ์ฆ ์š”๊ตฌ
ํ•„ํ„ฐ ์Šคํ‚ต ์กฐ๊ฑด์—์„œ /api/auth/signout์ œ๊ฑฐํ•˜๊ณ  JWT ํ•„ํ„ฐ๊ฐ€ ์ธ์ฆ ์„ธํŒ…์„ ํ•˜๋„๋ก ๋ณ€๊ฒฝ

if ("POST".equalsIgnoreCase(request.getMethod())
    && "/api/auth/signin".equals(request.getRequestURI())) {
    filterChain.doFilter(request, response);
    return;
}

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ† ํฐ์ด ์œ ํšจํ•  ๊ฒฝ์šฐ SecurityContext์— ์ธ์ฆ ์ •๋ณด๊ฐ€ ์„ธํŒ…๋˜๊ณ  hasRole("USER") ํ†ต๊ณผ

 


 

 

 

 

'Spring_๋ถ€์บ  > Trouble Tang Tang ๐Ÿ”ซ๐Ÿ”ซ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Spring] Trouble Gradle  (0) 2025.08.20
[Java] Level.3_Trouble Shooting  (0) 2025.07.17
[Java] Level.2_Trouble Shooting  (0) 2025.07.14
[Java] Level.1_Trouble Shooting  (3) 2025.07.10
[Spring] 2์ฃผ์ฐจ-clone trouble  (1) 2025.07.09