๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
์ƒ์„ฑ๋œ Namespace Service๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

Service ์‚ฌ์šฉ ๊ฐœ์š”

Namespace ํ˜ธ์ถœ

UserService.getUser()๊ฐ„๊ฒฐํ•œ ๋ฌธ๋ฒ•

ํƒ€์ž… ์•ˆ์ „

์ž๋™ ์™„์„ฑ์ปดํŒŒ์ผ ๊ฒ€์ฆ

Subset ์ง€์›

ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ์„ฑ๋Šฅ ์ตœ์ ํ™”

TanStack Query

useUser Hook์ž๋™ ์บ์‹ฑ

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

Service Import

์ƒ์„ฑ๋œ Service๋Š” services.generated.ts์—์„œ Namespace๋กœ export๋ฉ๋‹ˆ๋‹ค.
// โœ… ์˜ฌ๋ฐ”๋ฅธ import ๋ฐฉ์‹
import { UserService, PostService } from "@/services/services.generated";
๋‹จ์ผ ํŒŒ์ผ import์˜ ์žฅ์ :
  1. ์ผ๊ด€์„ฑ: ๋ชจ๋“  Service๊ฐ€ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌ๋จ
  2. ๊ฐ„ํŽธํ•œ import: ํŒŒ์ผ ๊ฒฝ๋กœ ์ฐพ์„ ํ•„์š” ์—†์Œ
  3. ์ž๋™ ์—…๋ฐ์ดํŠธ: pnpm generate ์‹œ ์ž๋™์œผ๋กœ ๋™๊ธฐํ™”
  4. ๋ช…ํ™•ํ•œ ๋„ค์ด๋ฐ: Namespace๋กœ ๊ทธ๋ฃนํ™”๋˜์–ด ์ถฉ๋Œ ์—†์Œ

๊ธฐ๋ณธ API ํ˜ธ์ถœ

Service์˜ ์ •์  ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
import { UserService } from "@/services/services.generated";

// ์‚ฌ์šฉ์ž ์กฐํšŒ (Subset "A"๋กœ ๊ธฐ๋ณธ ์ •๋ณด๋งŒ)
const user = await UserService.getUser("A", 123);
console.log(user.username); // ํƒ€์ž… ์•ˆ์ „!

// ์‚ฌ์šฉ์ž ์ˆ˜์ •
await UserService.updateProfile({
  username: "newname",
  bio: "Hello, World!",
});
ํŠน์ง•:
  • await์œผ๋กœ ๋น„๋™๊ธฐ ํ˜ธ์ถœ
  • ์‘๋‹ต์€ ์ž๋™์œผ๋กœ ํŒŒ์‹ฑ๋จ (fetch ํ•จ์ˆ˜ ๋‚ด๋ถ€ ์ฒ˜๋ฆฌ)
  • ํƒ€์ž…์ด ์™„์ „ํžˆ ๋ณด์žฅ๋จ
  • Subset ํŒŒ๋ผ๋ฏธํ„ฐ ํ•„์ˆ˜ (getUser ๋“ฑ)
Namespace ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ:
  • โŒ ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค: new UserService() ๋ถˆํ•„์š”
  • โœ… ์ •์  ํ•จ์ˆ˜: UserService.getUser() ์ง์ ‘ ํ˜ธ์ถœ
  • ๋ชจ๋“  ํ•จ์ˆ˜๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘

Subset ์‹œ์Šคํ…œ ์ดํ•ดํ•˜๊ธฐ

Sonamu์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ธ Subset ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค.
// Subset "A": ๊ธฐ๋ณธ ์ •๋ณด
const basicUser = await UserService.getUser("A", 123);
console.log(basicUser.id);       // โœ… OK
console.log(basicUser.username); // โœ… OK
console.log(basicUser.bio);      // โŒ ์ปดํŒŒ์ผ ์—๋Ÿฌ (Subset A์— ์—†์Œ)

// Subset "B": bio ํฌํ•จ
const userWithBio = await UserService.getUser("B", 123);
console.log(userWithBio.bio);    // โœ… OK

// Subset "C": ์ „์ฒด ํ•„๋“œ
const fullUser = await UserService.getUser("C", 123);
console.log(fullUser.createdAt); // โœ… OK
Subset์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ :
  1. ์„ฑ๋Šฅ: ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์กฐํšŒํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ ˆ๊ฐ
  2. ํƒ€์ž… ์•ˆ์ „: ๊ฐ Subset๋งˆ๋‹ค ์ •ํ™•ํ•œ ํƒ€์ž… ๋ฐ˜ํ™˜
  3. ๋ช…์‹œ์„ฑ: ์–ด๋–ค ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ์ง€ ์ฝ”๋“œ์—์„œ ๋ช…ํ™•ํžˆ ํ‘œํ˜„

React์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

TanStack Query Hook (๊ถŒ์žฅ)

๊ฐ€์žฅ ๊ฐ„ํŽธํ•œ ๋ฐฉ๋ฒ•์€ ์ž๋™ ์ƒ์„ฑ๋œ TanStack Query Hook์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
import { UserService } from "@/services/services.generated";

function UserProfile({ userId }: { userId: number }) {
  // ์ž๋™ ์ƒ์„ฑ๋œ Hook ์‚ฌ์šฉ
  const { data: user, error, isLoading } = UserService.useUser("A", userId);
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div>
      <h1>{user.username}</h1>
      <p>{user.email}</p>
    </div>
  );
}
TanStack Query Hook์˜ ์žฅ์ :
  • ์ž๋™ ์บ์‹ฑ (๊ฐ™์€ userId ์žฌ์‚ฌ์šฉ)
  • ์ž๋™ ์žฌ๊ฒ€์ฆ (ํฌ์ปค์Šค ์‹œ, ์žฌ์—ฐ๊ฒฐ ์‹œ)
  • ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ ์ž๋™ ๊ด€๋ฆฌ
  • ์ค‘๋ณต ์š”์ฒญ ์ž๋™ ์ œ๊ฑฐ

ํ•จ์ˆ˜ ์ปดํฌ๋„ŒํŠธ (useEffect)

Hook์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
import { useState, useEffect } from "react";
import { UserService } from "@/services/services.generated";
import type { User } from "@/services/services.generated";

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        // Subset "C"๋กœ ์ „์ฒด ํ•„๋“œ ์กฐํšŒ
        const userData = await UserService.getUser("C", userId);
        setUser(userData);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to load user");
      } finally {
        setLoading(false);
      }
    }
    
    fetchUser();
  }, [userId]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div>
      <h1>{user.username}</h1>
      <p>{user.email}</p>
    </div>
  );
}
๊ถŒ์žฅ: ๊ฐ€๋Šฅํ•˜๋ฉด TanStack Query Hook์„ ์‚ฌ์šฉํ•˜์„ธ์š”. ์ž๋™ ์บ์‹ฑ, ์žฌ๊ฒ€์ฆ, ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๋ชจ๋‘ ์ œ๊ณตํ•˜์—ฌ ์ฝ”๋“œ๊ฐ€ ํ›จ์”ฌ ๊ฐ„๊ฒฐํ•ด์ง‘๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ

์‚ฌ์šฉ์ž ์•ก์…˜์— ๋”ฐ๋ผ API๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
import { useState } from "react";
import { UserService } from "@/services/services.generated";

function EditProfile({ userId }: { userId: number }) {
  const [username, setUsername] = useState("");
  const [bio, setBio] = useState("");
  const [saving, setSaving] = useState(false);
  
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    
    try {
      setSaving(true);
      
      await UserService.updateProfile({
        username,
        bio,
      });
      
      alert("Profile updated!");
    } catch (error) {
      alert("Failed to update profile");
      console.error(error);
    } finally {
      setSaving(false);
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Username"
      />
      
      <textarea
        value={bio}
        onChange={(e) => setBio(e.target.value)}
        placeholder="Bio"
      />
      
      <button type="submit" disabled={saving}>
        {saving ? "Saving..." : "Save"}
      </button>
    </form>
  );
}

Next.js์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

Server Components (App Router)

Next.js 13+ App Router์˜ Server Components์—์„œ๋Š” Service๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ๋ฐฑ์—”๋“œ ๋ชจ๋ธ์„ ์ง์ ‘ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
// app/users/[id]/page.tsx

// โŒ Server Component์—์„œ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์Œ
// Service๋Š” ๋ธŒ๋ผ์šฐ์ €์šฉ fetch๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์„œ๋ฒ„์—์„œ ์‚ฌ์šฉ ๋ถˆ๊ฐ€
export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await UserService.getUser("A", parseInt(params.id)); // โŒ ์ž‘๋™ ์•ˆํ•จ!
  
  return <div>{user.username}</div>;
}
ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: ๋ฐฑ์—”๋“œ ๋ชจ๋ธ ์ง์ ‘ ์‚ฌ์šฉ (๊ถŒ์žฅ)
// app/users/[id]/page.tsx
import { UserModel } from "@/models/user.model";

export default async function UserPage({ params }: { params: { id: string } }) {
  // ์„œ๋ฒ„์—์„œ ๋ชจ๋ธ์„ ์ง์ ‘ ์‚ฌ์šฉ
  const userModel = new UserModel();
  const { user } = await userModel.getProfile(parseInt(params.id));
  
  return (
    <div>
      <h1>{user.username}</h1>
      <p>{user.email}</p>
    </div>
  );
}
๊ถŒ์žฅ ๋ฐฉ์‹: Server Components์—์„œ๋Š” ๋ฐฑ์—”๋“œ ๋ชจ๋ธ์„ ์ง์ ‘ ์‚ฌ์šฉํ•˜์„ธ์š”. HTTP ์š”์ฒญ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์—†๊ณ , ํƒ€์ž… ์•ˆ์ „์„ฑ๋„ ๋™์ผํ•˜๊ฒŒ ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.

Client Components

Client Components์—์„œ๋Š” Service๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
// app/users/[id]/edit-button.tsx
"use client";

import { useState } from "react";
import { UserService } from "@/services/services.generated";

export function EditButton({ userId }: { userId: number }) {
  const [editing, setEditing] = useState(false);
  
  async function handleEdit() {
    setEditing(true);
    
    try {
      await UserService.updateProfile({
        username: "newname",
      });
      
      alert("Updated!");
    } catch (error) {
      alert("Failed!");
    } finally {
      setEditing(false);
    }
  }
  
  return (
    <button onClick={handleEdit} disabled={editing}>
      {editing ? "Saving..." : "Edit"}
    </button>
  );
}

์—๋Ÿฌ ์ฒ˜๋ฆฌ

SonamuError ์ฒ˜๋ฆฌ

Service ํ˜ธ์ถœ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";

async function updateUser() {
  try {
    await UserService.updateProfile({
      username: "newname",
    });
  } catch (error) {
    if (isSonamuError(error)) {
      // Sonamu ์—๋Ÿฌ (ํƒ€์ž… ์•ˆ์ „)
      console.log("Status:", error.code);
      console.log("Message:", error.message);
      
      // Zod ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ
      error.issues.forEach((issue) => {
        console.log(`${issue.path.join(".")}: ${issue.message}`);
      });
      
      // HTTP ์ƒํƒœ ์ฝ”๋“œ๋ณ„ ์ฒ˜๋ฆฌ
      if (error.code === 401) {
        // ์ธ์ฆ ์—๋Ÿฌ
        console.log("Please login");
      } else if (error.code === 403) {
        // ๊ถŒํ•œ ์—๋Ÿฌ
        console.log("Permission denied");
      } else if (error.code === 422) {
        // Validation ์—๋Ÿฌ
        console.log("Invalid data:", error.issues);
      }
    } else {
      // ์ผ๋ฐ˜ ์—๋Ÿฌ
      console.log("Network error:", error);
    }
  }
}

React์—์„œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

import { isSonamuError } from "@/lib/sonamu.shared";

function EditProfile({ userId }: { userId: number }) {
  const [error, setError] = useState<string | null>(null);
  const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
  
  async function handleSubmit(data: any) {
    setError(null);
    setValidationErrors({});
    
    try {
      await UserService.updateProfile(data);
    } catch (err) {
      if (isSonamuError(err)) {
        setError(err.message);
        
        // Zod ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ๋ฅผ ํ•„๋“œ๋ณ„๋กœ ๋งคํ•‘
        const fieldErrors: Record<string, string> = {};
        err.issues.forEach((issue) => {
          const field = issue.path.join(".");
          fieldErrors[field] = issue.message;
        });
        setValidationErrors(fieldErrors);
      } else {
        setError("An unexpected error occurred");
      }
    }
  }
  
  return (
    <div>
      {error && <div className="error-message">{error}</div>}
      
      <form onSubmit={(e) => {
        e.preventDefault();
        handleSubmit({/* data */});
      }}>
        <div>
          <input name="username" />
          {validationErrors.username && (
            <span className="error">{validationErrors.username}</span>
          )}
        </div>
      </form>
    </div>
  );
}

๊ณ ๊ธ‰ ํŒจํ„ด

๋ณ‘๋ ฌ ์š”์ฒญ

์—ฌ๋Ÿฌ API๋ฅผ ๋™์‹œ์— ํ˜ธ์ถœํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.
import { UserService, PostService } from "@/services/services.generated";

async function loadUserDashboard(userId: number) {
  // โŒ ์ˆœ์ฐจ ์‹คํ–‰ (๋А๋ฆผ)
  const user = await UserService.getUser("A", userId);
  const posts = await PostService.getPostsByUser(userId);
  const comments = await PostService.getCommentsByUser(userId);
  
  return { user, posts, comments };
}

async function loadUserDashboardFast(userId: number) {
  // โœ… ๋ณ‘๋ ฌ ์‹คํ–‰ (๋น ๋ฆ„)
  const [user, posts, comments] = await Promise.all([
    UserService.getUser("A", userId),
    PostService.getPostsByUser(userId),
    PostService.getCommentsByUser(userId),
  ]);
  
  return { user, posts, comments };
}
์„ฑ๋Šฅ ๋น„๊ต:
  • ์ˆœ์ฐจ: 300ms + 200ms + 150ms = 650ms
  • ๋ณ‘๋ ฌ: max(300ms, 200ms, 150ms) = 300ms

Subset ํ™œ์šฉ ์ตœ์ ํ™”

์ƒํ™ฉ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ Subset์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
// ๋ชฉ๋ก ํ™”๋ฉด: ๊ธฐ๋ณธ ์ •๋ณด๋งŒ
const users = await UserService.getUsers("A");

users.map((user) => (
  <div key={user.id}>
    {user.username} - {user.email}
  </div>
));

// ์ƒ์„ธ ํ™”๋ฉด: ์ „์ฒด ์ •๋ณด
const fullUser = await UserService.getUser("C", userId);

<div>
  <h1>{fullUser.username}</h1>
  <p>{fullUser.bio}</p>
  <p>Created: {fullUser.createdAt}</p>
</div>

TanStack Query ํ™œ์šฉ

์กฐ๊ฑด๋ถ€ ํŽ˜์นญ

function UserProfile({ userId }: { userId: number | null }) {
  const { data: user } = UserService.useUser(
    "A",
    userId!,
    { enabled: userId !== null } // userId๊ฐ€ null์ด๋ฉด ํ˜ธ์ถœ ์•ˆํ•จ
  );
  
  if (!userId) return <div>Please select a user</div>;
  if (!user) return <div>Loading...</div>;
  
  return <div>{user.username}</div>;
}

์บ์‹œ ๋ฌดํšจํ™”

import { useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/services.generated";

function EditProfile({ userId }: { userId: number }) {
  const queryClient = useQueryClient();
  
  async function handleUpdate(data: any) {
    await UserService.updateProfile(data);
    
    // ํŠน์ • ์ฟผ๋ฆฌ ๋ฌดํšจํ™”
    queryClient.invalidateQueries(
      UserService.getUserQueryOptions("A", userId)
    );
    
    // ๋˜๋Š” ๋ชจ๋“  User ์ฟผ๋ฆฌ ๋ฌดํšจํ™”
    queryClient.invalidateQueries({
      queryKey: ["User"],
    });
  }
}

Prefetching

function UserList({ userIds }: { userIds: number[] }) {
  const queryClient = useQueryClient();
  
  // ํ˜ธ๋ฒ„ ์‹œ ๋ฏธ๋ฆฌ ๋กœ๋“œ
  function handleMouseEnter(userId: number) {
    queryClient.prefetchQuery(
      UserService.getUserQueryOptions("A", userId)
    );
  }
  
  return (
    <ul>
      {userIds.map((id) => (
        <li
          key={id}
          onMouseEnter={() => handleMouseEnter(id)}
        >
          User {id}
        </li>
      ))}
    </ul>
  );
}

์‹ค์ „ ์˜ˆ์ œ

์ „์ฒด CRUD ํ”Œ๋กœ์šฐ

import { useState } from "react";
import { UserService } from "@/services/services.generated";
import { isSonamuError } from "@/lib/sonamu.shared";

function UserManagement() {
  // ๋ชฉ๋ก ์กฐํšŒ (Subset A)
  const { data: users, refetch } = UserService.useUsers("A");
  
  // ์ƒ์„ฑ
  async function handleCreate(data: { username: string; email: string }) {
    try {
      await UserService.create(data);
      refetch(); // ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
      alert("Created!");
    } catch (error) {
      if (isSonamuError(error)) {
        alert(error.message);
      }
    }
  }
  
  // ์ˆ˜์ •
  async function handleUpdate(id: number, data: { username: string }) {
    try {
      await UserService.update(id, data);
      refetch();
      alert("Updated!");
    } catch (error) {
      if (isSonamuError(error)) {
        alert(error.message);
      }
    }
  }
  
  // ์‚ญ์ œ
  async function handleDelete(id: number) {
    if (!confirm("Are you sure?")) return;
    
    try {
      await UserService.delete(id);
      refetch();
      alert("Deleted!");
    } catch (error) {
      if (isSonamuError(error)) {
        alert(error.message);
      }
    }
  }
  
  return (
    <div>
      <h1>Users</h1>
      {users?.map((user) => (
        <div key={user.id}>
          {user.username}
          <button onClick={() => handleUpdate(user.id, { username: "new" })}>
            Edit
          </button>
          <button onClick={() => handleDelete(user.id)}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

์ฃผ์˜์‚ฌํ•ญ

Service ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ์ƒ์„ฑ๋œ Service ํŒŒ์ผ(services.generated.ts)์€ ์ ˆ๋Œ€ ์ˆ˜๋™ ์ˆ˜์ • ๊ธˆ์ง€
  2. Subset ํŒŒ๋ผ๋ฏธํ„ฐ ํ•„์ˆ˜: getUser ๋“ฑ์—์„œ subset ์ง€์ • ํ•„์š”
  3. Server Components์—์„œ๋Š” Service ๋Œ€์‹  ๋ฐฑ์—”๋“œ ๋ชจ๋ธ ์ง์ ‘ ํ˜ธ์ถœ
  4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ ์‹œ isSonamuError() ํƒ€์ž… ๊ฐ€๋“œ ์‚ฌ์šฉ
  5. TanStack Query Hook์€ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ๋งŒ ํ˜ธ์ถœ
  6. await ํ‚ค์›Œ๋“œ ํ•„์ˆ˜ (๋ชจ๋“  Service ํ•จ์ˆ˜๋Š” async)
  7. Namespace์ด๋ฏ€๋กœ new ๋ถˆํ•„์š”: UserService.getUser() ์ง์ ‘ ํ˜ธ์ถœ

๋‹ค์Œ ๋‹จ๊ณ„