카테고리 없음

백엔드 스터디 후기

myhousemouse 2026. 3. 22. 16:56

📌 기본 정보

  • 주차/주제: 3주차 - REST API 설계와 구현

📚 학습 내용 요약

  • API, REST API, RESTful API의 개념 정리하기API는 응용 프로그램(애플리케이션)에서 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 뜻합니다. 기본 개념:
    • 애플리케이션이 다른 애플리케이션에 요청을 보내고, 응답을 받을 수 있도록 정해진 규칙(메서드, 데이터 형식 등)을 제공합니다.
    • 예를 들어, 카카오톡 앱이 구글 지도 API를 사용해서 지도를 표시하는 것처럼, 다양한 프로그램들이 서로 서로의 기능을 호출하고 결과를 받습니다.✅ REST API(Representational State Transfer) 정의:웹상에서 사용되는 여러 리소스를 HTTP URI로 표현하고, 해당 리소스에 대한 행위를 HTTP Method로 정의하는 방식을 말합니다.
    •  기본 개념:
    • REST API 웹에서 데이터를 주고받기 위해 사용하는 API
    • HTTP 프로토콜을 사용하여 데이터를 주고받는 방식을 정의합니다.
    • REST API는 URL을 통해 리소스를 지정하고, HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 리소스에 대한 동작을 수행합니다.
    • 리소스는 데이터 객체일 수 있고, 이를 HTTP 메서드 조회, 생성, 수정, 삭제합니다.
    • 구성요소
    1. 자원(Resource) : HTTP URI
    2. 자원에 대한 행위(Verb) : HTTP Method
    3. 자원에 대한 행위의 내용 (Representations) : HTTP Message Pay Load, 데이터 형식RESTful API REST 아키텍처 스타일을 잘 따르는 APIAPI를 RESTful 하게 만들어서 API의 목적이 무엇인지 명확하게 하기 위해 RESTful 함을 지향 합니다.
    4.  기본 개념:
    5. 즉, REST API를 설계할 때 좋은 방식으로 설계된 API를 의미
    6. ✅ RESTful API 정의:
    • RESTful API REST 원칙과 규칙을 충실히 따르는 API입니다.
      • /(슬래시)는 계층 관계를 나타낼때 사용합니다.
      • URI 마지막 문자에 /(슬래시)를 사용하지 않습니다.
      • URI에 _(underscore)는 사용하지 않도록 합니다. 또한 영어 대문자보다는 소문자를 씁니다.
      • URI에 명사를 사용합니다.
      • URI에 파일의 확장자(예를들어 .json , .JPGE)를 포함 시키지 않습니다.
    • 각 리소스는 URL로 고유하게 식별되며, HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 리소스를 조작합니다.
    • 상태를 유지하지 않으며, 각 요청은 독립적으로 처리됩니다. 즉, 서버는 각 요청에 대해 필요한 정보만을 처리하고 이전 요청을 기억하지 않습니다.
  • 주로 파일 제어, 창 제어, 화상 처리, 문자 제어 등을 위한 인터페이스를 제공합니다
  •  API(Application Programming Interface) 정의:

 RESTful API 특징:

  1. 자원(Resource): 모든 데이터는 자원(리소스)으로 취급됩니다.
    - 예: 사용자(users), 게시글(posts), 제품(products)
  2. HTTP 메서드: 각 자원에 대해 GET, POST, PUT, DELETE 메서드를 사용해 읽기, 생성, 수정, 삭제 작업을 수행합니다.

  1. 무상태성(Stateless): 각 요청은 독립적이며, 서버는 이전 요청을 기억하지 않습니다.
  • 사용자가 로그인하지 않은 상태에서 GET /users 요청을 보내면, 서버는 사용자 인증 정보를 기억하지 않음. 클라이언트는 매번 인증 정보를 요청에 담아 보내야 합니다.
  1. 유연한 응답 형식: JSON이나 XML 형식으로 데이터를 응답받을 수 있습니다.
  2. Uniform Interface (인터페이스 일관성): RESTful API는 일관된 인터페이스를 유지하며, URL 구조나 메서드 사용을 통해 명확한 규칙따름.
  • 리소스 이름은 복수형
  1. Layered System (계층화)
  • HTTP 메서드(GET, POST, PUT, DELETE)의 용도와 차이점 정리하기HTTPRequestMethod 용도:POST 메서드는 새로운 리소스(데이터)를 서버에 생성할 때 사용됩니다. 즉, 서버에서 새로운 데이터를 추가하거나, 새로운 객체를 생성할 때 쓰는 메서드입니다.✅ 주요 특징:
    • 리소스 생성: 클라이언트가 서버에 요청을 보내고, 서버는 새로운 리소스를 생성하여 저장합니다.
    • 유일한 ID: 서버에서 새로운 리소스를 생성할 때, 해당 리소스에 고유 ID를 부여하는 경우가 많습니다.
    • 본문(body): POST 요청은 데이터를 요청 본문(body)에 담아 보냅니다. 이 데이터는 주로 JSON 형식입니다.
    • 멱등성 없음: 같은 요청을 여러 번 보내면 매번 다른 결과를 초래할 수 있습니다. (즉, 중복 생성이 될 수 있음)
    •  장점:
    • 서버에 데이터를 추가하는 데 적합합니다.
    • 데이터를 요청 본문에 담아 보낼 수 있어 복잡한 데이터도 함께 전송 가능합니다.
    • 요청이 서버 상태를 변경하기 때문에, 응답으로 생성된 리소스를 반환하는 것이 일반적입니다.
  •  POST : 생성, 서버에 자원을 등록

  • 새 유저 추가: /users에 POST 요청을 보내면 새로운 유저가 생성됩니다.
@PostMapping
    public String createUser(@RequestBody User user) {
        service.createUser(user);
        return "사용자가 저장되었습니다.";
    }
POST /users
body : {date : "example"}
Content-Type : "application/json"

 GET: 읽기, 서버 자원을 얻음

 용도:

GET 메서드는 주로 데이터를 조회하는 데 사용됩니다.

즉, 서버에서 데이터를 읽기만 하고, 수정하거나 삭제하지는 않습니다.

 주요 특징:

  • 데이터 조회: 클라이언트가 서버에 요청을 보내고, 서버는 데이터를 응답으로 보내줍니다.
  • 멱등성(Idempotency): 같은 요청을 여러 번 보내도 결과가 변하지 않아요. 예를 들어, /coffees를 여러 번 요청해도, 항상 같은 커피 목록이 응답으로 돌아옵니다.
  • URL 경로: 조회할 데이터를 경로로 구분해 보냅니다. 예를 들어:
    • /coffees → 모든 커피 목록 조회
    • /coffees/{id} → 특정 ID의 커피 조회

 장점:

  • 단순하고 직관적: 데이터를 요청하는 방식이 매우 직관적이라서 이해하기 쉽습니다.
  • 캐싱 가능: GET 요청은 서버나 브라우저에서 캐싱될 수 있어, 동일한 요청을 더 빠르게 처리할 수 있습니다.

  • 모든 유저 조회: /users/에 GET 요청을 보내면 유저 목록을 받을 수 있습니다.
  • 특정 유저 조회: /users/{id}에 GET 요청을 보내면, 그 특정 유저 정보를 받을 수 있습니다.
@GetMapping("/")
    public List<User> getAllUsers() {
        return service.getAllUsers();  // 모든 사용자 목록 반환
    }
@GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return service.getUserById(id);
    }

**● PUT: 업데이트, 서버 자원을 새로운 자원으로 치환**

 용도:

PUT 메서드는 기존 리소스를 수정하거나, 없는 리소스는 새로 생성할 때 사용됩니다.

즉, 서버에 있는 기존 데이터를 업데이트하거나, 존재하지 않으면 새 데이터를 추가할 수 있습니다.

 주요 특징:

  • 기존 리소스 수정: 클라이언트가 요청한 URI에 해당하는 기존 데이터를 수정합니다.
  • 리소스 생성 가능: 만약 해당 리소스가 존재하지 않으면 새로운 리소스를 생성할 수 있습니다.
  • 멱등성(Idempotency): 같은 PUT 요청을 여러 번 보내도 결과가 변하지 않음. 즉, 한 번 수정된 데이터는 같은 PUT 요청을 여러 번 보내도 결과가 동일합니다.
  • 본문(body): PUT 요청은 데이터를 요청 본문(body)에 담아 보내며, 주로 JSON 형식으로 데이터를 보냅니다

 장점:

  • 리소스 수정에 적합합니다.
  • 불필요한 중복 요청을 피할 수 있어, 같은 요청을 여러 번 보내도 데이터는 동일하게 유지됩니다.
  • 데이터 업데이트와 생성을 하나의 메서드로 처리할 수 있어 간편합니다.

  • 유저 정보 수정: /users/{id}에 PUT 요청을 보내면 해당 유저 정보를 수정합니다.
  • 새로운 유저 생성: /users/{id}에 PUT 요청을 보내면, 해당 ID 유저가 없으면 새로 생성합니다.
@PutMapping("/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        service.updateUser(user);
        return "사용자 전체 수정 완료!";
    }
PUT /user/1
body : {date : "update example"}
Content-Type : "application/json"

 PATCH: 업데이트, 서버 자원의 일부만 수정

 용도:

PATCH 메서드는 리소스의 일부만 수정할 때 사용됩니다.

즉, 기존 리소스를 완전히 교체하지 않고 일부 속성만 업데이트할 때 사용해요.

 주요 특징:

  • 부분 수정: 리소스의 전체가 아닌 일부만 수정합니다. 예를 들어, 커피의 이름만 바꾸고 가격은 그대로 두는 식입니다.
  • 멱등성 없음: PATCH 요청은 멱등성을 보장하지 않아요. 즉, 같은 PATCH 요청을 여러 번 보내면 결과가 달라질 수 있습니다. (수정된 값이 여러 번 적용될 수 있음)
  • 본문(body): PATCH 요청은 수정할 데이터만 본문에 담아 보냅니다. 주로 JSON 형식으로 보내며, 수정할 부분만 포함시킵니다.
  • 적용 범위: PUT은 전체 리소스를 대체하지만, PATCH 부분적인 수정만 진행해요.

 장점:

  • 부분 수정을 통해 불필요한 데이터 변경을 피할 수 있습니다.
  • 데이터의 일부만 변경하고 나머지는 그대로 두고 싶을 때 적합합니다.
  • 사용자의 정보 일부 수정: /users/{id}에 PATCH 요청을 보내면 특정 정보만 업데이트됩니다.
@PatchMapping("/{id}")
    public String partialUpdateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
        service.partialUpdateUser(id, updates);
        return "사용자 일부 수정 완료!";
    }

PUT과 PATCH의 차이점과 멱등성

 PUT의 멱등성

PUT 리소스 전체를 대체하는 방식

첫 번째 요청 이후에 같은 데이터를 다시 보내면 결과가 변하지 않음

 PATCH의 멱등성

PATCH 부분 수정을 하는 메서드

PATCH 요청이 들어올 때마다 부분적인 값이 계속 수정될 수 있음

PUT 리소스 전체를 대체하고, 결과가 항상 같게 유지되므로 멱등성 보장됨.

PATCH 부분만 수정하기 때문에, 같은 요청을 여러 번 보낼 경우 결과가 달라질 수 있어 멱등성 보장되지 않음.

 DELETE: 삭제, 서버 자원을 삭제

 용도:

DELETE 메서드는 리소스를 삭제할 때 사용됩니다.

즉, 서버에서 특정 데이터를 삭제하는 요청을 처리합니다.

 주요 특징:

  • 리소스 삭제: 클라이언트가 서버에 요청을 보내면, 서버는 특정 리소스 삭제합니다.
  • 멱등성(Idempotency): DELETE 멱등성을 보장합니다. 즉, 같은 DELETE 요청을 여러 번 보내도, 결과가 같고 삭제된 리소스는 다시 복구되지 않습니다.
  • URI 경로: DELETE 요청은 보통 삭제할 리소스의 URI를 지정해 보내며, 리소스를 찾아서 삭제합니다.
  • 응답 상태 코드: 삭제가 성공하면 204 No Content 응답을 보낼 수 있고, 리소스가 없으면 404 Not Found를 보낼 수 있습니다.

 장점:

  • 리소스 삭제를 간단하고 명확하게 처리할 수 있습니다.
  • 멱등성이 보장되므로 여러 번 삭제 요청을 보내도 결과가 동일합니다.
  • 사용자 삭제: /users/{id}에 DELETE 요청을 보내면, 해당 사용자를 삭제합니다.
@DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        service.deleteUser(id);
        return "사용자 삭제 완료!";
    }

 OPTIONS: 통신옵션 조회 (리소스가 지원하고 있는 메소드의 취득)
 HEAD: 응답헤더 조회(메타 데이터의 취득)

  • 간단한 API 기능 2개 구상해오기 (ex: 사용자 조회, 게시글 작성 등)
    1. 도서 추가 (POST /books)
POST /books
{
    "title": "자바의 정석",
    "author": "남궁성",
    "price": 25000,
    "category": "Programming"
}

2. 도서 목록 조회 (GET /books)

GET /books?category=Programming&price_max=30000
[
    {
        "id": 1,
        "title": "자바의 정석",
        "author": "남궁성",
        "price": 25000,
        "category": "Programming"
    },
    {
        "id": 2,
        "title": "파이썬 완벽 가이드",
        "author": "홍길동",
        "price": 20000,
        "category": "Programming"
    }
]

💡 새롭게 알게 된 점 & 어려웠던 부분

  • 새롭게 알게된 점: API 개념을 다시 정확하게 알게 되었고, HTTP 메서드와 API 기능에 대해 더 깊게 탐구하고 싶어졌습니다.
  • 어려웠던 부분: HTTP 메서드와 API 기능을 더 깊게 이해하는 부분이 어려웠습니다.
  • 해결 방법: HTTP 메서드와 API 기능에 대해 더 많은 예제와 실습을 통해 이해를 돕고, 구체적인 동작 방식을 연구해볼 계획입니다.

🚀 과제/실습 결과

- Spring Boot
- Gradle
- Jsp templeate
- H2 데이터베이스
- JDK 17

### 🛠️설정
    plugins {
        id 'java'
        id 'org.springframework.boot' version '3.4.4'
        id 'io.spring.dependency-management' version '1.1.7'
    }
    
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(17)
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        runtimeOnly 'com.h2database:h2'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
    
    test {
        useJUnitPlatform()
    }
# H2 메모리 데이터베이스 설정
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 콘솔 사용 설정
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# 초기 SQL 실행
spring.sql.init.mode=always
  • schema 설정
CREATE TABLE users (
                       id BIGINT AUTO_INCREMENT PRIMARY KEY,
                       name VARCHAR(100),
                       email VARCHAR(100)
);

CREATE TABLE posts (
                       id BIGINT AUTO_INCREMENT PRIMARY KEY,
                       title VARCHAR(200),
                       content TEXT,
                       user_id BIGINT,
                       FOREIGN KEY (user_id) REFERENCES users(id)
);

사용자조회 기능

1. Model

package com.example.userapi.model;
public class User {
    private Long id;
    private String name;
    private String email;

    public User() {} // Spring이 객체를 만들 때 기본 생성자를 사용

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getter / Setter
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}
  • id
  • name
  • email

2. Controller

package com.example.userapi.controller;

import com.example.userapi.model.User;
import com.example.userapi.service.UserService;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService service;

    public UserController(UserService service) {
        this.service = service;
    }

    @GetMapping("/")
    public List<User> getAllUsers() {
        return service.getAllUsers();  // 모든 사용자 목록 반환
    }

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return service.getUserById(id);
    }

    @PostMapping
    public String createUser(@RequestBody User user) {
        service.createUser(user);
        return "사용자가 저장되었습니다.";
    }

    @PutMapping("/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        user.setId(id);
        service.updateUser(user);
        return "사용자 전체 수정 완료!";
    }

    @PatchMapping("/{id}")
    public String partialUpdateUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
        service.partialUpdateUser(id, updates);
        return "사용자 일부 수정 완료!";
    }

    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        service.deleteUser(id);
        return "사용자 삭제 완료!";
    }
}

3. Service

package com.example.userapi.service;
import com.example.userapi.model.User;
import com.example.userapi.repository.UserRepository;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository repo;

    public UserService(UserRepository repo) {
        this.repo = repo;
    }

    public void createUser(User user) {
        repo.save(user);
    }

    public List<User> getAllUsers() {
        return repo.findAll();  // 모든 사용자 목록 반환
    }

    public User getUserById(Long id) {
        return repo.findById(id)
                .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다."));
    }

    public void updateUser(User user) {
        repo.update(user);
    }

    public void partialUpdateUser(Long id, Map<String, Object> updates) {
        User user = getUserById(id);
        if (updates.containsKey("name")) {
            user.setName((String) updates.get("name"));
        }
        if (updates.containsKey("email")) {
            user.setEmail((String) updates.get("email"));
        }
        repo.update(user);
    }

    public void deleteUser(Long id) {
        repo.deleteById(id);
    }
}

4. Repository

package com.example.userapi.repository;
import com.example.userapi.model.User;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public class UserRepository {

    private final JdbcTemplate jdbc;

    public UserRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public void save(User user) {
        jdbc.update("INSERT INTO users (name, email) VALUES (?, ?)",
                user.getName(), user.getEmail());
    }

    public List<User> findAll() {
        return jdbc.query("SELECT * FROM users", userRowMapper());
    }

    public Optional<User> findById(Long id) {
        // jdbc.query()를 통해 결과를 리스트로 받고, 그 리스트에서 첫 번째 값을 Optional로 반환
        List<User> result = jdbc.query("SELECT * FROM users WHERE id = ?",
                new Object[]{id},
                userRowMapper());
        return result.stream().findAny();  // 결과가 있으면 첫 번째 항목을 Optional로 반환
    }

    public void update(User user) {
        jdbc.update("UPDATE users SET name = ?, email = ? WHERE id = ?",
                user.getName(), user.getEmail(), user.getId());
    }

    public void deleteById(Long id) {
        jdbc.update("DELETE FROM users WHERE id = ?", id);
    }

    private RowMapper<User> userRowMapper() {
        return (rs, rowNum) -> new User(
                rs.getLong("id"),
                rs.getString("name"),
                rs.getString("email")
        );
    }
}

게시글 게시 기능

1. Model

package com.example.userapi.model;

public class Post {
    private Long id;
    private String title;
    private String content;
    private Long userId;  // 작성자 ID

    public Post() {}

    public Post(Long id, String title, String content, Long userId) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.userId = userId;
    }

    // Getter/Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }

    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }

    public Long getUserId() { return userId; }
    public void setUserId(Long userId) { this.userId = userId; }
}
  • id
  • title
  • content
  • userId

2. Controller

package com.example.userapi.controller;
import com.example.userapi.model.Post;
import com.example.userapi.service.PostService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/posts")
public class PostController {

    private final PostService service;

    public PostController(PostService service) {
        this.service = service;
    }

    @PostMapping
    public String createPost(@RequestBody Post post) {
        service.createPost(post);
        return "게시글이 저장되었습니다.";
    }

    @GetMapping("/{id}")
    public Post getPost(@PathVariable Long id) {
        return service.getPost(id);
    }
}

3. Service

package com.example.userapi.service;
import com.example.userapi.model.Post;
import com.example.userapi.repository.PostRepository;
import com.example.userapi.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public class PostService {

    private final PostRepository postRepo;
    private final UserRepository userRepo;

    public PostService(PostRepository postRepo, UserRepository userRepo) {
        this.postRepo = postRepo;
        this.userRepo = userRepo;
    }

    public void createPost(Post post) {
        // 작성자 존재 여부 확인 (옵션)
        userRepo.findById(post.getUserId())
                .orElseThrow(() -> new RuntimeException("작성자(User)를 찾을 수 없습니다."));
        postRepo.save(post);
    }

    public Post getPost(Long id) {
        return postRepo.findById(id)
                .orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없습니다."));
    }
}

4. Repository

package com.example.userapi.repository;

import com.example.userapi.model.Post;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class PostRepository {

    private final JdbcTemplate jdbc;

    public PostRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public void save(Post post) {
        jdbc.update("INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)",
                post.getTitle(), post.getContent(), post.getUserId());
    }

    public Optional<Post> findById(Long id) {
        return jdbc.query("SELECT * FROM posts WHERE id = ?",
                new Object[]{id},
                rs -> {
                    if (rs.next()) {
                        return Optional.of(new Post(
                                rs.getLong("id"),
                                rs.getString("title"),
                                rs.getString("content"),
                                rs.getLong("user_id")
                        ));
                    } else {
                        return Optional.empty();
                    }
                });
    }
}

<사용자 조회, 게시글 플로우차트>

👥 스터디 피드백

  • 받은 피드백:
  1. REST API 설계에서 가장 중요한 원칙은 무엇인가요?
    일관성이 가장 중요하며, URL, 메서드, 응답 형식 등을 일관되게 설계하면 개발자들이 API를 쉽게 사용할 수 있습니다.
  2. HTTP 상태 코드의 의미와 중요성
    상태 코드는 요청 결과를 나타내며, 404 Not Found, 500 Internal Server Error 등의 코드가 오류를 파악하고 디버깅에 중요합니다. 각 코드의 숫자는 상태를 쉽게 파악할 수 있기 때문입니다.
  3. API 성능 최적화 방법
    캐싱과 상태 코드 출력을 통해 응답 속도를 높이고, 쿼리 최적화를 통해 성능을 개선할 수 있습니다.