ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜

 

 

 

 ํ”„๋กœ์ ํŠธ์˜ ์ดˆ๊ธฐ ๋ฒ„์ „์—๋Š” ๋”ฐ๋กœ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„๊ฐ€ ์—†์—ˆ๊ณ , React Native์—์„œ Firebase๋ฅผ ์ด์šฉํ•ด ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ๋‚˜๋Š” ์ดํ›„ ํ”„๋กœ์ ํŠธ์— ํ•ฉ๋ฅ˜ํ•ด Spring์œผ๋กœ ๋‹ค๋ฅธ ์„œ๋ฒ„๋ฅผ ๊ฐœ๋ฐœํ•˜๊ณ  ์žˆ๋‹ค. ๋ฐฑ์—”๋“œ์—์„œ๋„ ์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•˜๊ธฐ ์œ„ํ•ด Firebase๋กœ ๋ฐœ๊ธ‰๋œ ํ† ํฐ์„ ์‚ฌ์šฉํ•ด ํ”„๋ก ํŠธ์—”๋“œ์™€ ์ธ์ฆ ์ฒด๊ณ„๋ฅผ ํ†ตํ•ฉํ•  ํ•„์š”๊ฐ€ ์žˆ์—ˆ๋‹ค.

 

 ๊ตฌํ˜„์„ ์œ„ํ•ด Firebase Token์— ๋Œ€ํ•ด ์ดํ•ดํ•˜๋Š” ๊ณผ์ •์ด ํ•„์š”ํ–ˆ๊ณ , ํ† ํฐ์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด Spring Security๋„ ํ•™์Šตํ•ด์•ผ ๋๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ตœ์ข…์ ์œผ๋กœ firebase๊ฐ€ ํฌํ•จ๋œ ์„œ๋ฒ„๋ฅผ ๋ฐฐํฌํ•˜๊ธฐ๊นŒ์ง€ ๋งŽ์€ ์‚ฝ์งˆ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค. ๊ตฌํ˜„ํ•˜๋ฉด์„œ ๊ณต๋ถ€ํ–ˆ๋˜ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๊ณ , ๊ตฌํ˜„ ๊ณผ์ •์„ ๊ณต์œ ํ•˜๊ณ ์ž ํ•œ๋‹ค!

 

 

 

 

 

Firebase Token?


 ์šฐ์„  Firebase Token์— ๋Œ€ํ•ด ์ดํ•ด์•ผํ•˜ ํ•œ๋‹ค. Firebase Authentication์„ ํ†ตํ•ด ๋กœ๊ทธ์ธ์„ ํ•˜๋ฉด ๋ฐœ๊ธ‰๋˜๋Š” ํ† ํฐ์œผ๋กœ JWT(JSON Web Token) ํ˜•์‹์ด๋‹ค. Firebase SDK์—์„œ ๋กœ๊ทธ์ธ๊ณผ ์„ธ์…˜ ์œ ์ง€ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ID Token(Access Token)๊ณผ Refresh Token์˜ ์ˆ˜๋ช…์„ ๊ด€๋ฆฌํ•˜๊ณ , ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

 

 ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ์œ„ํ•ด ๋ฐœ๊ธ‰๋˜๊ณ  ์‚ฌ์šฉ๋˜๋Š” ์ •๋ณด์—๋Š” UID, ID Token, Refresh Token์ด ์žˆ๋‹ค.

 ์ด 3๊ฐœ์˜ ๊ฐœ๋…๊ณผ ์“ฐ์ž„์ƒˆ๋ฅผ ๋ช…ํ™•ํžˆ ์•Œ์•„์•ผ ํ•œ๋‹ค. ํ˜ผ๋™๋˜๊ธฐ ์‰ฝ๋‹ค.

 

 

 

UID

  • Firebase Authentication์—์„œ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ฐœ๊ธ‰ํ•œ ๊ณ ์œ ํ•œ ID
  • ๋ฐฑ์—”๋“œ์—์„œ Firebase Admin SDK๋กœ ํ† ํฐ ๊ฒ€์ฆ ๋ฐ UID ์ถ”์ถœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

ID Token(Access Token)

  • 1์‹œ๊ฐ„ ๋™์•ˆ ์œ ํšจํ•˜๋ฏ€๋กœ ๋งŒ๋ฃŒ๋˜๋ฉด ์ƒˆ๋กœ ๊ฐฑ์‹ ํ•ด์•ผ ํ•œ๋‹ค.
  • ์„œ๋ฒ„์— ์ธ์ฆ ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค.
  • ๋ฐฑ์—”๋“œ์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ์š”์ฒญ ํ—ค๋”๋กœ ์ „๋‹ฌ๋ฐ›์€ ID Token์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

 

Refresh Token

  • ID Token์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ, Firebase์—์„œ ์ƒˆ๋กœ์šด ID Token์„ ๋ฐœ๊ธ‰๋ฐ›๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
  • Firebase SDK๊ฐ€ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์ค€๋‹ค.( = ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ Refresh Token ๊ด€๋ฆฌํ•  ํ•„์š” ์—†์Œ)

 

 

 

 

 

์ธ์ฆ ๊ณผ์ •


 ํด๋ผ์ด์–ธํŠธ์™€ ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž, Firebase ์‚ฌ์ด์—์„œ ์ด๋ฃจ์–ด์ง€๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ์˜ ์ž์„ธํ•œ ๊ณผ์ •์€ ์ƒ๋žตํ•˜๊ณ , ๋กœ๊ทธ์ธ ์ดํ›„ ํด๋ผ์ด์–ธํŠธ, Firebase, ์„œ๋ฒ„์™€์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์ค‘์‹ฌ์œผ๋กœ ์„ค๋ช…ํ•˜๊ฒ ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ณผ์ •์œผ๋กœ ์ธ์ฆ ๋ฐ ์š”์ฒญ์ด ์ด๋ฃจ์–ด ์ง„๋‹ค.

 

 

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

 

 

1. ํด๋ผ์ด์–ธํŠธ์—์„œ ์†Œ์…œ ๋กœ๊ทธ์ธ์„ ์š”์ฒญํ•˜๋ฉด Firebase๋Š” ์†Œ์…œ ๋กœ๊ทธ์ธ ์ œ๊ณต์ž๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ํ† ํฐ์„ ๊ฒ€์ฆํ•œ๋‹ค.

2. Firebase๋Š” ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ID Token๊ณผ Refresh Token์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

3. ํด๋ผ์ด์–ธํŠธ๋Š” ID Token์„ ์ธ์ฆ ํ—ค๋”๋กœ ๋ฐฑ์—”๋“œ ์„œ๋ฒ„์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.

4. ๋ฐฑ์—”๋“œ๋Š” Firebase Admin SDK๋ฅผ ์‚ฌ์šฉํ•ด ํ•ด๋‹น ํ† ํฐ์„ ๊ฒ€์ฆํ•œ๋‹ค.

5. Firebases๋Š” ๊ฒ€์ฆ์— ์„ฑ๊ณตํ•˜๋ฉด UID๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

6. ์„œ๋ฒ„๋Š” UID๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•˜๊ณ  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

 

 

 

 

 

Spring Security


์ถœ์ฒ˜ : https://spring.io/projects

 

 

 ์„œ๋ฒ„๋กœ ๋“ค์–ด์˜ค๋Š” ๋ชจ๋“  ์š”์ฒญ์„ Firebase ํ† ํฐ์œผ๋กœ ๊ฒ€์ฆํ•˜๊ณ , ์ ‘๊ทผ์„ ์ œ์–ดํ•˜๋Š” ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด์•ผ ํ•œ๋‹ค. Spring์—์„œ ์ธ์ฆ, ์ธ๊ฐ€ ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์ฃผ๋Š” Spring Security๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

 Spring Security๋Š” Filter๋ฅผ ํ†ตํ•ด ์š”์ฒญ์ด Servlet์— ๋„๋‹ฌํ•˜๊ธฐ ์ „์— Firebase ํ† ํฐ์„ ๊ฒ€์ฆํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฒ€์ฆ์ด ๋˜๋ฉด, SecurityContextHolder์— UID๋‚˜ ์ด๋ฉ”์ผ ๋“ฑ์˜ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ์ธ์ฆ ๊ฐ์ฒด(Authentification)์„ ์ €์žฅํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์„œ๋ฒ„์˜ ์ „์—ญ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ์ฐธ์กฐํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

 

 

 

 

 

๊ตฌํ˜„


๊ฐœ๋ฐœ ํ™˜๊ฒฝ

์„œ๋ฒ„

  • Spring Boot 3.2.4, Amazon corretto 17.0.10
  • Firebase
  • Spring Security
  • Lombok

 

์ธํ”„๋ผ

  • GCP, Ubuntu
  • Docker

 

 

 

 

 

1.  ์˜์กด์„ฑ ์ถ”๊ฐ€

build.gradle

implementation 'com.google.firebase:firebase-admin:9.2.0'
implementation 'org.springframework.boot:spring-boot-starter-security'

 

  • Firebase Admin ์‚ฌ์šฉ์„ ์œ„ํ•œ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
  • Spring Security ์‚ฌ์šฉ์„ ์œ„ํ•œ ์˜์กด์„ฑ๋„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

 

 

 

 

 

 2.  Firebase key ์ƒ์„ฑ

 Firebase ํ”„๋กœ์ ํŠธ์—์„œ ๋“ค์–ด๊ฐ€์„œ 'ํ”„๋กœ์ ํŠธ ์„ค์ •' > '์„œ๋น„์Šค ๊ณ„์ •' > 'Firebase Admin SDK' > '์ƒˆ ๋น„๊ณต๊ฐœ ํ‚ค ์ƒ์„ฑ' ์„ ํด๋ฆญํ•˜๋ฉด, json์œผ๋กœ ๋œ key๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

 

 

 3.  Firebase ์„ค์ • ํด๋ž˜์Šค ์ถ”๊ฐ€

@Configuration
public class FirebaseConfig {

    @Bean
    public FirebaseApp initializeFirebase() throws IOException {
        FirebaseOptions options = FirebaseOptions.builder()
                .setCredentials(GoogleCredentials.fromStream(new ClassPathResource("serviceAccountKey.json").getInputStream()))
                .build();

        if (FirebaseApp.getApps().isEmpty()) {
            return FirebaseApp.initializeApp(options);
        } else {
            return FirebaseApp.getInstance();
        }
    }
}

 

 

key์˜ ํŒŒ์ผ ๊ฒฝ๋กœ์— json ํŒŒ์ผ์ด ์žˆ๋Š” ๊ฒฝ๋กœ๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

 

๊ฐœ๋ฐœํ•˜๋Š” ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋Š” src/main/resources/serviceAccountKey.json์˜€๋‹ค. ํ•˜์ง€๋งŒ ํด๋ผ์šฐ๋“œ์— docker๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฐฐํฌํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋‚  ๊ฒƒ์ด๋‹ค.

 

Failed to instantiate [com.google.firebase.FirebaseApp]: Factory method 'initializeFirebase' threw exception with message: src/main/resources/serviceAccountKey.json (No such file or directory)

 

๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ๋‹ค.

 

//AS-IS
FileInputStream serviceAccount = new FileInputStream("src/main/resources/serviceAccountKey.json");
FirebaseOptions options = FirebaseOptions.builder()
	.setCredentials(GoogleCredentials.fromStream(serviceAccount))
    .build();
    
//TO-BE
FirebaseOptions options = FirebaseOptions.builder()
	.setCredentials(GoogleCredentials.fromStream(new ClassPathResource("serviceAccountKey.json").getInputStream()))
	.build();

 

 

 

 

 

 4.  Firebase ์ธ์ฆ ํ•„ํ„ฐ ํด๋ž˜์Šค

 Firebase ์ธ์ฆ ํ† ํฐ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ Spring Security์˜ Custom Filter์ด๋‹ค. Servlet์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„ ์ธ์ฆ ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค. 

 

@Component
public class FirebaseAuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

        String authHeader = httpRequest.getHeader("Authorization");

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.replace("Bearer ", ""); // "Bearer " ์ดํ›„์˜ ํ† ํฐ ๊ฐ’๋งŒ ์ถ”์ถœ

            // Firebase Token์„ ๊ฒ€์ฆํ•˜๊ณ  UID๋ฅผ ์ถ”์ถœ
			FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(token);
			String uid = decodedToken.getUid();

            // UID๋ฅผ ์ด์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑ
            User principal = new User(uid, "", Collections.emptyList());
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList());

            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));

            // Spring Security์˜ SecurityContext์— ์„ค์ •
            SecurityContextHolder.getContext().setAuthentication(authentication);

        }

        // ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ์š”์ฒญ์„ ๋„˜๊น€
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

 

 

1. doFilter ๋ฉ”์†Œ๋“œ๋Š” ํ•„ํ„ฐ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ํ˜ธ์ถœ๋œ๋‹ค.

2. ์š”์ฒญ ํ•ด๋”์—์„œ Firebase์˜ JWT๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ Bearer [ํ† ํฐ] ํ˜•์‹์œผ๋กœ ์ „๋‹ฌ์ด ๋œ๋‹ค.

3. ํ† ํฐ ๊ฐ’(ID Token)์„ ์ถ”์ถœํ•˜๊ณ , Firebase Admin SDK๋กœ ํ† ํฐ์„ ๊ฒ€์ฆํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ ํ† ํฐ์ด ์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

4. ์ธ์ฆ๋˜์—ˆ์œผ๋ฉด, UID๋ฅผ ์ถ”์ถœํ•œ๋‹ค.

5. UID๋ฅผ ์‚ฌ์šฉ์ž id๋กœ ์„ค์ •ํ•ด Spring Security์˜ Authentification ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

6. SecurityContext์— ์ธ์ฆ ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค.

6. ํ•„ํ„ฐ ์ฒด์ธ์—์„œ ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ์š”์ฒญ์„ ๋„˜๊ธฐ๊ฑฐ๋‚˜ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ์ „๋‹ฌํ•œ๋‹ค.

 

 

 

 

 

 5.  Security ์„ค์ • ํด๋ž˜์Šค ์ถ”๊ฐ€

 Spring Security ์„ค์ • ํด๋ž˜์Šค์—์„œ Firebase ์ธ์ฆ ํ•„ํ„ฐ๋ฅผ ์ •์˜ํ•˜๊ณ , ์„œ๋ฒ„์˜ ๋ณด์•ˆ ๊ทœ์น™์„ ์„ค์ •ํ•œ๋‹ค.

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final FirebaseAuthenticationFilter firebaseAuthenticationFilter;

    public SecurityConfig(FirebaseAuthenticationFilter firebaseAuthenticationFilter) {
        this.firebaseAuthenticationFilter = firebaseAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())  // CSRF ๋น„ํ™œ์„ฑํ™”
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**")
                        .permitAll()
                        .requestMatchers("/test/**")
                        .permitAll()
                        .anyRequest().authenticated()  // ๊ทธ ์™ธ ์š”์ฒญ์€ ์ธ์ฆ ํ•„์š”
                )
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ
                )
                .addFilterBefore(firebaseAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)  // Firebase ์ธ์ฆ ํ•„ํ„ฐ ์ถ”๊ฐ€
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();  // ํŒจ์Šค์›Œ๋“œ ์ธ์ฝ”๋” ์„ค์ •
    }
}

 

 

authorizeHttpRequests

  • ์š”์ฒญ URL์— ๋Œ€ํ•ด ์ ‘๊ทผ ์ œ์–ด๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ํŠน์ • ๊ฒฝ๋กœ๋Š” permitAll()์„ ํ†ตํ•ด ์ธ์ฆ ์—†์ด ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋งŒ์•ฝ Swagger๋ฅผ ์“ด๋‹ค๋ฉด, ๊ด€๋ จ๋œ ๊ฒฝ๋กœ๋“ค์„ ํ—ˆ์šฉํ•ด์ค˜์•ผ ํ•œ๋‹ค. ์•ˆ ํ•ด์ฃผ๋ฉด Swagger์— ์ ‘์†ํ•  ์ˆ˜ ์—†๋‹ค.

 

์„ธ์…˜ ๊ด€๋ฆฌ ์ •์ฑ…

  • Firebase๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ† ํฐ ๊ธฐ๋ฐ˜์˜ ์ธ์ฆ์„ ํ•˜๋ฏ€๋กœ, STATELESS๋กœ ์„ค์ •ํ•œ๋‹ค.

 

Spring Security ํ•„ํ„ฐ ์ฒด์ธ์— ์ปค์Šคํ…€ ํ•„ํ„ฐ(firebaseAuthenticationFilter)๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

giraffe_