πŸ” Spring Boot둜 κ΅¬ν˜„ν•˜λŠ” μ†Œμ…œ 둜그인과 JWT 인증 μ‹œμŠ€ν…œ

TwoweekheeΒ·2025λ…„ 5μ›” 20일

μ•ˆλ…•ν•˜μ„Έμš”, 개발자 μ—¬λŸ¬λΆ„! μ˜€λŠ˜μ€ μ œκ°€ μ΅œκ·Όμ— μ™„μ„±ν•œ Spring Boot 기반의 μ†Œμ…œ 둜그인 ν”„λ‘œμ νŠΈμ— λŒ€ν•΄ κ³΅μœ ν•΄λ³΄λ €κ³  ν•©λ‹ˆλ‹€. μš”μ¦˜ λŒ€λΆ€λΆ„μ˜ μ„œλΉ„μŠ€μ—μ„œ μ†Œμ…œ λ‘œκ·ΈμΈμ€ ν•„μˆ˜μ μΈ κΈ°λŠ₯이 λ˜μ—ˆμ£ . μ‚¬μš©μž κ²½ν—˜μ„ κ°œμ„ ν•˜κ³  λ³΄μ•ˆμ„ κ°•ν™”ν•˜λŠ” μ†Œμ…œ 둜그인 μ‹œμŠ€ν…œμ„ μ–΄λ–»κ²Œ κ΅¬ν˜„ν–ˆλŠ”μ§€ ν•¨κ»˜ μ‚΄νŽ΄λ³΄μ•„μš”!

상세 μ½”λ“œ (κΌ­ 보고.. star..뢀탁)

https://github.com/twoweekhee/oauth2-demo

πŸ“š λͺ©μ°¨

  1. ν”„λ‘œμ νŠΈ κ°œμš”
  2. 기술 μŠ€νƒ
  3. OAuth2 & Security κ΅¬ν˜„ 상세
  4. 인증 ν”„λ‘œμ„ΈμŠ€ 흐름
  5. 토큰 관리 방식
  6. λ³΄μ•ˆ κ°•ν™” 포인트
  7. ν”„λ‘œμ νŠΈ μ„€μ • 방법
  8. 마치며

ν”„λ‘œμ νŠΈ κ°œμš”

우리 νŒ€μ€ μ‚¬μš©μž 인증 μ‹œμŠ€ν…œμ„ κ°œλ°œν•˜λ©΄μ„œ λ³΄μ•ˆμ„±κ³Ό μ‚¬μš©μž κ²½ν—˜ 두 마리 토끼λ₯Ό λͺ¨λ‘ 작고 μ‹Άμ—ˆμ–΄μš”. 🐰🐰 κ·Έλž˜μ„œ κ°€μž₯ 많이 μ‚¬μš©λ˜λŠ” μ†Œμ…œ 둜그인(Google, Kakao, Naver)을 λͺ¨λ‘ μ§€μ›ν•˜κ³ , JWT 기반의 인증 μ‹œμŠ€ν…œμ„ λ„μž…ν–ˆλ‹΅λ‹ˆλ‹€.

이 ν”„λ‘œμ νŠΈμ˜ μ£Όμš” κΈ°λŠ₯은 λ‹€μŒκ³Ό κ°™μ•„μš”:

  • πŸ”‘ μ†Œμ…œ 둜그인 (Google, Kakao, Naver)
  • 🎫 JWT 기반 인증 (Access Token, Refresh Token)
  • πŸ”’ λ³΄μ•ˆ μ„€μ • (CORS, CSRF 관리)
  • πŸͺ μΏ ν‚€ 기반 토큰 관리
  • πŸ“ μ‚¬μš©μž μ•½κ΄€ λ™μ˜ ν”„λ‘œμ„ΈμŠ€

기술 μŠ€νƒ

ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•˜κΈ° μ „, μ΅œμ‹  기술 μŠ€νƒμ„ 선택해 μ•ˆμ •μ μ΄κ³  ν™•μž₯ κ°€λŠ₯ν•œ μ‹œμŠ€ν…œμ„ κ΅¬μΆ•ν•˜λ €κ³  λ…Έλ ₯ν–ˆμ–΄μš”. μ‚¬μš©ν•œ κΈ°μˆ μ€ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€:

  • Java 21
  • Spring Boot 3.4.5
  • Spring Security
  • Spring OAuth2 Client
  • JWT (jjwt 0.12.3)
  • MySQL
  • Redis
  • QueryDSL
  • Lombok

특히 Java 21κ³Ό Spring Boot 3.4.5λ₯Ό μ„ νƒν•œ μ΄μœ λŠ” μ΅œμ‹  κΈ°λŠ₯λ“€κ³Ό μ„±λŠ₯ κ°œμ„ μ„ ν™œμš©ν•˜κΈ° μœ„ν•΄μ„œμ˜€μ–΄μš”. πŸš€ λ˜ν•œ Redisλ₯Ό μ‚¬μš©ν•΄ 토큰 κ΄€λ¦¬μ˜ νš¨μœ¨μ„±μ„ λ†’μ˜€μŠ΅λ‹ˆλ‹€.

OAuth2 & Security κ΅¬ν˜„ 상세

OAuth2 μ†Œμ…œ 둜그인 κ΅¬ν˜„μ€ 크게 μ„Έ λ‹¨κ³„λ‘œ λ‚˜λˆ„μ–΄μ„œ μ§„ν–‰ν–ˆμ–΄μš”.

OAuth2 κ΅¬ν˜„ 방식

λ¨Όμ €, Spring Security OAuth2 Clientλ₯Ό μ‚¬μš©ν•΄ 각 μ†Œμ…œ 둜그인 제곡자(Google, Kakao, Naver)λ₯Ό μ„€μ •ν–ˆμ–΄μš”. 각 μ œκ³΅μžλ³„λ‘œ ν•„μš”ν•œ scope와 user-info-uriλ₯Ό 적절히 κ΅¬μ„±ν•˜λŠ” 것이 μ€‘μš”ν–ˆμŠ΅λ‹ˆλ‹€.

μ†Œμ…œ 둜그인 인증 ν”„λ‘œμ„ΈμŠ€λŠ” λ‹€μŒκ³Ό 같은 μ»΄ν¬λ„ŒνŠΈλ‘œ κ΅¬μ„±λ˜μ–΄ μžˆμ–΄μš”:

  1. CustomAuthorizationRequestResolver: OAuth2 인증 μš”μ²­μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.
  2. CustomOAuth2UserService: μ†Œμ…œ 둜그인 μ‚¬μš©μž 정보λ₯Ό 가져와 μ²˜λ¦¬ν•©λ‹ˆλ‹€.
  3. OAuth2AuthenticationSuccessHandler: 인증 성곡 μ‹œ JWT 토큰을 λ°œκΈ‰ν•©λ‹ˆλ‹€.
  4. OAuth2AuthenticationFailureHandler: 인증 μ‹€νŒ¨ μ‹œ μ μ ˆν•œ μ—λŸ¬ 처리λ₯Ό ν•©λ‹ˆλ‹€.

특히 둜그인 성곡 ν•Έλ“€λŸ¬μ—μ„œλŠ” μ‚¬μš©μž 정보λ₯Ό 기반으둜 JWT 토큰을 μƒμ„±ν•˜κ³ , 이λ₯Ό 쿠킀에 μ•ˆμ „ν•˜κ²Œ μ €μž₯ν•˜λŠ” λ‘œμ§μ„ κ΅¬ν˜„ν–ˆμ–΄μš”. 😎

Security κ΅¬ν˜„ 방식

Spring Security 섀정은 λ³΄μ•ˆκ³Ό ν™•μž₯성을 κ³ λ €ν•΄ λ‹€μŒκ³Ό 같이 κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) {
        return http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(AbstractHttpConfigurer::disable)
            .formLogin(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/private/**").authenticated()
                .anyRequest().denyAll())
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(endpoint -> endpoint
                    .authorizationRequestResolver(customAuthorizationRequestResolver))
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService))
                .successHandler(oAuth2AuthenticationSuccessHandler)
                .failureHandler(oAuth2AuthenticationFailureHandler))
            .build();
    }
}

이 섀정은 REST API 기반 μ„œλΉ„μŠ€μ— 맞게 μ΅œμ ν™”λ˜μ–΄ μžˆμ–΄μš”. CSRF λ³΄ν˜ΈλŠ” λΉ„ν™œμ„±ν™”ν–ˆμ§€λ§Œ, λŒ€μ‹  JWT 토큰과 HttpOnly μΏ ν‚€λ‘œ λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ μ„Έμ…˜μ„ STATELESS둜 μ„€μ •ν•΄ μ„œλ²„μ˜ λΆ€ν•˜λ₯Ό 쀄이고 ν™•μž₯성을 λ†’μ˜€μ–΄μš”. πŸ“ˆ

인증 ν”„λ‘œμ„ΈμŠ€ 흐름

μ‚¬μš©μž 인증 과정이 μ–΄λ–»κ²Œ μ΄λ£¨μ–΄μ§€λŠ”μ§€ μ’€ 더 μžμ„Ένžˆ μ‚΄νŽ΄λ³ΌκΉŒμš”?

μ†Œμ…œ 둜그인 κ³Όμ •

  1. μ‚¬μš©μžκ°€ ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ "Google둜 둜그인" λ²„νŠΌμ„ ν΄λ¦­ν•©λ‹ˆλ‹€.
  2. /api/public/oauth2/google μ—”λ“œν¬μΈνŠΈλ‘œ μš”μ²­μ΄ μ „μ†‘λ©λ‹ˆλ‹€.
  3. μ‚¬μš©μžλŠ” Google 둜그인 νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈλ©λ‹ˆλ‹€.
  4. μ‚¬μš©μžκ°€ Googleμ—μ„œ 둜그인 인증을 μ™„λ£Œν•©λ‹ˆλ‹€.
  5. Google은 인증 μ½”λ“œμ™€ ν•¨κ»˜ 우리 μ„œλΉ„μŠ€μ˜ 콜백 URL둜 λ¦¬λ‹€μ΄λ ‰νŠΈν•©λ‹ˆλ‹€.
  6. μ„œλ²„μ—μ„œλŠ” 이 μ½”λ“œλ₯Ό μ‚¬μš©ν•΄ Google APIμ—μ„œ μ‚¬μš©μž 정보λ₯Ό νšλ“ν•©λ‹ˆλ‹€.
  7. μ‚¬μš©μž 정보λ₯Ό 기반으둜 JWT 토큰(Access Token, Refresh Token)을 μƒμ„±ν•©λ‹ˆλ‹€.
  8. 토큰은 HttpOnly 쿠킀에 μ €μž₯되고, μ‚¬μš©μžλŠ” 메인 νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈλ©λ‹ˆλ‹€.

이 과정은 Kakao와 Naver λ‘œκ·ΈμΈμ—λ„ λ™μΌν•˜κ²Œ μ μš©λ©λ‹ˆλ‹€. 단지 μ œκ³΅μžλ³„ API μ—”λ“œν¬μΈνŠΈμ™€ μ‚¬μš©μž 정보 ν˜•μ‹λ§Œ λ‹€λ₯Ό λΏμ΄μ—μš”. πŸ”„

νšŒμ›κ°€μž… κ³Όμ •

μ†Œμ…œ λ‘œκ·ΈμΈμ„ 처음 μ‹œλ„ν•˜λŠ” μ‚¬μš©μžλŠ” μžλ™μœΌλ‘œ νšŒμ›κ°€μž… ν”„λ‘œμ„ΈμŠ€λ‘œ μ•ˆλ‚΄λ©λ‹ˆλ‹€:

  1. μ†Œμ…œ 둜그인 성곡 ν›„, μ‹œμŠ€ν…œμ€ μ‚¬μš©μžκ°€ μ‹ κ·œμΈμ§€ κΈ°μ‘΄ μ‚¬μš©μžμΈμ§€ ν™•μΈν•©λ‹ˆλ‹€.
  2. μ‹ κ·œ μ‚¬μš©μžμΈ 경우, μ•½κ΄€ λ™μ˜ νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈλ©λ‹ˆλ‹€.
  3. μ‚¬μš©μžκ°€ 약관에 λ™μ˜ν•˜λ©΄, λ‹€μŒ 정보가 μ €μž₯λ©λ‹ˆλ‹€:
    • μ†Œμ…œ 계정 정보 (이메일, μ†Œμ…œ ID, 제곡자 정보)
    • μ‚¬μš©μž μ—­ν•  (기본적으둜 ROLE_USER)
    • μ•½κ΄€ λ™μ˜ 정보 (λ™μ˜ μ‹œκ°„, μ•½κ΄€ 버전 λ“±)
  4. νšŒμ›κ°€μž…μ΄ μ™„λ£Œλ˜λ©΄ JWT 토큰이 λ°œκΈ‰λ˜κ³ , μ‚¬μš©μžλŠ” 메인 νŽ˜μ΄μ§€λ‘œ μ•ˆλ‚΄λ©λ‹ˆλ‹€.

μ΄λ ‡κ²Œ μ‚¬μš©μžλŠ” λ³„λ„μ˜ νšŒμ›κ°€μž… 폼을 μž‘μ„±ν•  ν•„μš” 없이 μ†Œμ…œ κ³„μ •λ§ŒμœΌλ‘œ λΉ λ₯΄κ²Œ μ„œλΉ„μŠ€μ— κ°€μž…ν•  수 μžˆμ–΄μš”. πŸ‘

토큰 관리 방식

JWT 토큰 κ΄€λ¦¬λŠ” λ³΄μ•ˆμ˜ 핡심이죠! πŸ˜‰ 우리 ν”„λ‘œμ νŠΈμ—μ„œλŠ” Access Tokenκ³Ό Refresh Token을 ν•¨κ»˜ μ‚¬μš©ν•΄ λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμŠ΅λ‹ˆλ‹€.

토큰 ꡬ성

  • Access Token: μ‹€μ œ API 접근에 μ‚¬μš©λ˜λ©°, 30λΆ„μ˜ 짧은 만료 μ‹œκ°„μ„ κ°€μ§‘λ‹ˆλ‹€.
  • Refresh Token: Access Token 갱신에 μ‚¬μš©λ˜λ©°, 2일의 만료 μ‹œκ°„μ„ κ°€μ§‘λ‹ˆλ‹€.

토큰 κ°±μ‹  κ³Όμ •

  1. μ‚¬μš©μžμ˜ Access Token이 만료되면(30λΆ„ ν›„), ν΄λΌμ΄μ–ΈνŠΈλŠ” μžλ™μœΌλ‘œ 토큰 갱신을 μš”μ²­ν•©λ‹ˆλ‹€.
  2. μ„œλ²„λŠ” μΏ ν‚€μ—μ„œ Refresh Token을 ν™•μΈν•˜κ³ , Redis에 μ €μž₯된 κ°’κ³Ό 비ꡐ해 μœ νš¨μ„±μ„ κ²€μ¦ν•©λ‹ˆλ‹€.
  3. μœ νš¨ν•œ 경우, μƒˆλ‘œμš΄ Access Token을 λ°œκΈ‰ν•˜κ³  쿠킀에 μ €μž₯ν•©λ‹ˆλ‹€.
  4. Refresh Token은 계속 μœ νš¨ν•˜λ―€λ‘œ κ·ΈλŒ€λ‘œ μœ μ§€λ©λ‹ˆλ‹€.

이런 λ°©μ‹μœΌλ‘œ μ‚¬μš©μžλŠ” 2일 λ™μ•ˆ 둜그인 μƒνƒœλ₯Ό μœ μ§€ν•  수 μžˆμœΌλ©΄μ„œλ„, μ‹€μ œ API μ ‘κ·Ό κΆŒν•œ(Access Token)은 30λΆ„λ§ˆλ‹€ κ°±μ‹ λ˜μ–΄ λ³΄μ•ˆμ΄ κ°•ν™”λ©λ‹ˆλ‹€.

λ³΄μ•ˆ κ°•ν™” 포인트

이 ν”„λ‘œμ νŠΈμ—μ„œλŠ” λ‹€μ–‘ν•œ λ°©λ²•μœΌλ‘œ λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμ–΄μš”:

  1. HttpOnly μΏ ν‚€: XSS κ³΅κ²©μœΌλ‘œλΆ€ν„° 토큰을 λ³΄ν˜Έν•©λ‹ˆλ‹€.
  2. Stateless μ„Έμ…˜ 관리: μ„œλ²„ λΆ€ν•˜λ₯Ό 쀄이고 ν™•μž₯성을 λ†’μž…λ‹ˆλ‹€.
  3. Redisλ₯Ό ν™œμš©ν•œ 토큰 관리: Refresh Token을 μ•ˆμ „ν•˜κ²Œ κ΄€λ¦¬ν•˜κ³ , ν•„μš”μ‹œ μ¦‰μ‹œ λ¬΄νš¨ν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  4. CORS μ„€μ •: ν—ˆμš©λœ μ˜€λ¦¬μ§„μ—μ„œλ§Œ API μš”μ²­μ΄ κ°€λŠ₯ν•˜λ„λ‘ μ œν•œν•©λ‹ˆλ‹€.

특히 λ‘œκ·Έμ•„μ›ƒ μ‹œμ—λŠ” λ‹€μŒκ³Ό 같은 ν”„λ‘œμ„ΈμŠ€λ‘œ λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμŠ΅λ‹ˆλ‹€:

// λ‘œκ·Έμ•„μ›ƒ 처리 μ½”λ“œ 일뢀
Cookie deletedAccessCookie = cookieProvider.generateDeletedAccessTokenCookie();
Cookie deletedRefreshCookie = cookieProvider.generateDeletedRefreshTokenCookie();
response.addCookie(deletedAccessCookie);
response.addCookie(deletedRefreshCookie);
// Redisμ—μ„œ Refresh Token μ‚­μ œ
// SecurityContext μ΄ˆκΈ°ν™”

μ΄λ ‡κ²Œ ν΄λΌμ΄μ–ΈνŠΈ 쿠킀와 μ„œλ²„ Redis μ €μž₯μ†Œ λͺ¨λ‘μ—μ„œ 토큰을 μ œκ±°ν•΄ μ™„λ²½ν•œ λ‘œκ·Έμ•„μ›ƒμ„ κ΅¬ν˜„ν–ˆμ–΄μš”. πŸ›‘οΈ

ν”„λ‘œμ νŠΈ μ„€μ • 방법

이 ν”„λ‘œμ νŠΈλ₯Ό 직접 싀행해보고 μ‹ΆμœΌμ‹ κ°€μš”? λ‹€μŒ 단계λ₯Ό λ”°λΌν•΄λ³΄μ„Έμš”:

ν•„μˆ˜ ν™˜κ²½ λ³€μˆ˜ μ„€μ •

# OAuth2 Client Credentials
GOOGLE_CLIENT_ID=${YOUR_GOOGLE_CLIENT_ID}
GOOGLE_CLIENT_SECRET=${YOUR_GOOGLE_CLIENT_SECRET}
KAKAO_CLIENT_ID=${YOUR_KAKAO_CLIENT_ID}
KAKAO_CLIENT_SECRET=${YOUR_KAKAO_CLIENT_SECRET}
NAVER_CLIENT_ID=${YOUR_NAVER_CLIENT_ID}
NAVER_CLIENT_SECRET=${YOUR_NAVER_CLIENT_SECRET}

# JWT Keys
ACCESS_TOKEN_KEY=${YOUR_ACCESS_TOKEN_KEY}
REFRESH_TOKEN_KEY=${YOUR_REFRESH_TOKEN_KEY}

# Database
MYSQL_URL=jdbc:mysql://localhost:3306/oauth2_demo
MYSQL_USER_NAME=${YOUR_MYSQL_USERNAME}
MYSQL_PASSWORD=${YOUR_MYSQL_PASSWORD}

각 μ†Œμ…œ 둜그인 제곡자의 개발자 μ½˜μ†”μ—μ„œ ν΄λΌμ΄μ–ΈνŠΈ ID와 μ‹œν¬λ¦Ώμ„ λ°œκΈ‰λ°›μ•„ μ„€μ •ν•΄μ£Όμ„Έμš”.

μ‹€ν–‰ 방법

  1. MySQLκ³Ό Redis μ„œλ²„λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.
  2. ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.
  3. λ‹€μŒ λͺ…λ Ήμ–΄λ‘œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‹€ν–‰ν•©λ‹ˆλ‹€:
./gradlew bootRun
  1. λΈŒλΌμš°μ €μ—μ„œ http://localhost:8080에 접속해 μ†Œμ…œ 둜그인 κΈ°λŠ₯을 ν…ŒμŠ€νŠΈν•΄λ³΄μ„Έμš”!

API λ¬Έμ„œλŠ” Swagger UIλ₯Ό 톡해 확인할 수 μžˆμ–΄μš”: http://localhost:8080/api/swagger-ui/index.html

마치며

μ†Œμ…œ 둜그인과 JWT 인증을 κ΅¬ν˜„ν•˜λ©΄μ„œ 배운 점이 정말 λ§Žμ•˜μ–΄μš”. μ‚¬μš©μž κ²½ν—˜κ³Ό λ³΄μ•ˆ μ‚¬μ΄μ˜ κ· ν˜•μ„ λ§žμΆ”λŠ” 것이 κ°€μž₯ μ–΄λ €μ› μ§€λ§Œ, Spring Security와 OAuth2 Client의 κ°•λ ₯ν•œ κΈ°λŠ₯ 덕뢄에 μ•ˆμ •μ μΈ μ‹œμŠ€ν…œμ„ ꡬ좕할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

λ‹€μŒ λ‹¨κ³„λ‘œλŠ” μ†Œμ…œ 둜그인 정보λ₯Ό ν™œμš©ν•œ μ‚¬μš©μž ν”„λ‘œν•„ 관리와, κΆŒν•œ 기반의 μ„Έλ°€ν•œ μ ‘κ·Ό μ œμ–΄ μ‹œμŠ€ν…œμ„ κ΅¬ν˜„ν•  κ³„νšμ΄μ—μš”. πŸš€

μ—¬λŸ¬λΆ„λ„ 이 글이 μ†Œμ…œ 둜그인 κ΅¬ν˜„μ— 도움이 λ˜μ—ˆμœΌλ©΄ μ’‹κ² λ„€μš”! κΆκΈˆν•œ μ μ΄λ‚˜ κ°œμ„ ν•  뢀뢄이 μžˆλ‹€λ©΄ λŒ“κΈ€λ‘œ μ•Œλ €μ£Όμ„Έμš”. ν•¨κ»˜ μ„±μž₯ν•΄μš”! πŸ‘‹


μ°Έκ³ : 이 글에 μ‚¬μš©λœ μ½”λ“œλŠ” 데λͺ¨ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μœ„ν•œ 것이며, μ‹€μ œ ν”„λ‘œλ•μ…˜ ν™˜κ²½μ—μ„œλŠ” 좔가적인 λ³΄μ•ˆ 섀정이 ν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

profile
κ°œλ°œν•˜λŠ” 감자, 이세상 감자 λͺ¨μ—¬λΌ~

0개의 λŒ“κΈ€