관리 메뉴

나만을 위한 블로그

[Android] 레트로핏 예제 - 서버에서 값 가져와 앱에서 보여주기 본문

Android

[Android] 레트로핏 예제 - 서버에서 값 가져와 앱에서 보여주기

참깨빵위에참깨빵 2020. 10. 5. 22:57
728x90
반응형

Retrofit, Volley와 관련해서 연계된 포스팅을 작성할 계획이다.

글을 작성하면서 공부하지 않고 넘겼던 부분이 있다면 공부하기 위해서 시리즈를 기획하게 됐다.

순서는 아래와 같다. 레트로핏이 뭔지는 예전에 작성했었으니 이전 포스팅을 참고하자.

 

1. 레트로핏이란?

 

[Android] Retrofit(레트로핏)이란?

참고한 사이트 : http://devflow.github.io/retrofit-kr/ Retrofit - 한글 문서 A type-safe HTTP client for Android and Java devflow.github.io https://galid1.tistory.com/617 Java - Retrofit이란? (retrofi..

onlyfor-me-blog.tistory.com

2. 레트로핏 예제 - 서버에서 값 가져와 앱에서 보여주기

3. 레트로핏으로 앱에서 CRUD하는 방법 < 1 >

4. 레트로핏으로 앱에서 CRUD하는 방법 < 2 >

 

 

아래에서 설명하는 내용은 모두 개인의 주관이며, 코드 작성법 또한 정석이 아니다. 이런 방식으로도 쓴다는 것을 보여주고 나 혼자 공부하기 위해 기록하는 글이다. 무조건 이 글이 정답은 아닌 것에 주의하자.

이번 포스팅의 주제는 안드로이드에서 레트로핏을 사용해 서버에 있는 데이터들을 가져와 보여주는 것이다.

서버는 AWS를 사용할 것이지만 xampp 등 APM 한 방 패키지를 사용중이라면 그걸 써도 상관없다.

아래는 각각 사용한 툴이다.

 

  • MySQL 접속 툴 = phpmyadmin과 HeidiSQL
  • PHP 파일 에디터 = 비주얼 스튜디오 코드
  • 쿼리 결과 확인 = 포스트맨

 

< 진행 순서 >

 

1. DB, 테이블 생성

2. PHP 파일 코딩

3. 안드로이드 xml, 자바 파일 코딩

 

 

1) DB, 테이블 생성

 

 

먼저 더미 데이터를 담을 데이터베이스와 테이블을 생성하겠다.

예제 수준의 포스팅이기 때문에 DB명은 example, 테이블은 person으로 대충 지었다.

테이블 컬럼은 PK인 id, 이름을 담을 name, 취미를 담을 hobby의 3개 컬럼만 만든다.

 

phpmyadmin에서 이렇게 한 다음 만들기를 누르면 자동으로 테이블 만드는 화면으로 이동한다.

테이블 이름을 넣고 컬럼 수를 정한 뒤 실행을 누르면 테이블의 상세 설정을 할 수 있다.

 

실행을 누르면 아래 화면으로 이동한다.

적당히 설정하고 저장을 누르면 테이블이 완성되면서 아래와 같은 화면이 나온다.

 

됐다. 이제 INSERT문을 써서 더미 데이터를 몇 개 입력한다.

데이터를 INSERT한 후 보기 버튼을 누르면 아래의 화면이 나온다.

 

이제 PHP 파일을 만들어보자.

 

 

2) PHP 파일 코딩

 

 

비주얼 스튜디오 코드를 써서 자신의 웹 서버에 연결한다.

그 후 SELECT문으로 DB에 있는 데이터들을 꺼내오는 내용을 코딩한다.

그 전에 DB에 접속하는 connect 파일부터 작성해야 한다. 이게 없으면 DB 접속 자체가 불가능하다.

파일명은 example_con.php로 지었다.

 

<?php

$con = mysqli_connect("xx.xxx.xx.xxx", "root", "비밀번호", "example");

각자 서버 주소와 MySQL 비밀번호를 입력해준다. DB명을 example로 따라서 지었다면 그 부분은 수정할 필요가 없다.

이제 SELECT 쿼리문을 사용하는 PHP 파일을 코딩할 차례다.

코딩 전에 생각해볼 게 있다. 바로 앱에서 어떻게 작동할지를 먼저 정하는 것이다. 이걸 정하지 않고 코딩하면 나중에 2번 일을 하게 될 수 있으니 중요한 작업이다.

내 계획은 메인 액티비티에서 이름과 취미를 입력한 후 버튼을 누르면 다음 액티비티로 이동해, 그곳에서 editText에 입력한 내용을 DB에서 찾아 뿌려주는 식으로 작동하는 예제 앱을 만드는 것이다.

그러려면 name, hobby 컬럼에 맞게 editText를 2개 만들어야 하고 앱에서 이 2가지 String을 서버의 PHP 파일로 전달해, 쿼리문을 실행시켜 결과를 받아서 앱으로 돌려줘야 한다. 마지막으로 서버에서 받은 결과를 앱에 뿌려주면 끝이다.

그럼 이제 PHP 파일을 코딩해보자. 파일명은 example_select.php로 지었다.

 

<?php

header("Content-type:application/json");

require_once 'example_con.php';

// $name = $_GET['name'];
// $hobby = $_GET['hobby'];

$sql = "SELECT * FROM person WHERE name = 'Kim' AND hobby = 'music'";

$result = mysqli_query($con, $sql);

$data = mysqli_num_rows($result);

if ($data > 0)
{
    $error = "ok";
    echo json_encode(array("response" => $error, "name" => "Kim", "hobby" => "music"));
    // echo json_encode(array("response" => $error, "name" => $name, "hobby" => $hobby));
}
else
{
    $error = "failed";
    echo json_encode(array("response" => $error));
}

mysqli_close($con);

앱을 먼저 코딩한 것이 아니기 때문에 일단 하드코딩으로 때웠다. 나중에 앱을 코딩하고 결과를 확인할 때쯤 되면 주석친 부분을 지우고 $error = "ok"; 밑줄을 조금 수정하면 된다.

저 상태로 포스트맨에 PHP 파일 경로를 넣고 돌리면 아래와 같은 결과가 나온다.

 

name, hobby를 알맞게 찾아서 JSON 형식으로 반환하는 걸 볼 수 있다.

PHP 파일도 정상 작동하는 걸 확인했으니 이제 마지막 단계인 앱 코딩이 남았다.

 

 

3) 안드로이드 xml, 자바 파일 코딩

 

 

레트로핏을 쓰려면 앱 수준 gradle에 의존성을 넣어야 한다. 아래의 문장 3개를 앱 수준 gradle에 넣어주자.

 

implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'

정상적으로 복붙했다면 아래와 똑같은 형태가 되어야 한다.

 

노란 줄이 뜨는 이유는 2.6.1이 최신 버전이라 그것으로 바꾸라고 표시되는 것이다. 무시해도 된다.

그리고 매니페스트에 인터넷 퍼미션을 추가해준다. 당연히 웹 서버에 있는 데이터를 가져와 보여줘야 하는데 인터넷 퍼미션이 없다면 앱이 작동하지 않는다. 아래의 문장을 매니페스트에 추가해주자.

<uses-permission android:name="android.permission.INTERNET" />

아래 화면처럼 보여야 한다.

 

그리고 res 폴더 안에 xml 폴더를 만들고 파일을 하나 생성한 다음, 매니페스트에 요소를 추가해야 한다.

이것에 대한 설명은 예전에 썼던 포스팅에 써 뒀으니 그걸 참고하자.

onlyfor-me-blog.tistory.com/118

 

[Android] Volley 사용 시 com.android.volley.noconnectionerror java.io.ioexception cleartext http traffic to 'ip 주소' not p

참고한 사이트 : https://developside.tistory.com/85 안드로이드 http 프로토콜 접속 시 예외발생 조치 (ERR CLEARTEXT NOT PERMITTED) 어제 앱을 개발 중 glide v4를 사용하여 웹에 있는 그림을 load 하였는데,..

onlyfor-me-blog.tistory.com

 

이제 밑준비는 끝났으니 XML 파일부터 코딩해보자.

앱 화면은 적당히 가운데에 editText 2개 만들고 버튼 하나 눌러서 다음 액티비티로 이동할 수 있게 만들 것이기 때문에 대충 만든다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="100dp"
        android:layout_marginStart="30dp"
        android:layout_marginEnd="30dp"
        android:weightSum="1">
        
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".3"
            android:text="이름"
            android:textSize="20sp"
            android:gravity="end"/>
        
        <EditText
            android:id="@+id/edit_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content" 
            android:layout_weight=".7"
            android:hint="이름 입력"
            android:layout_marginStart="20dp"/>
        
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="100dp"
        android:layout_marginStart="30dp"
        android:layout_marginEnd="30dp"
        android:weightSum="1">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".3"
            android:text="취미"
            android:textSize="20sp"
            android:gravity="end"/>

        <EditText
            android:id="@+id/edit_hobby"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".7"
            android:hint="취미 입력"
            android:layout_marginStart="20dp"/>

    </LinearLayout>
    
    <Button
        android:id="@+id/search_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:text="검 색"
        android:textSize="20sp"
        android:layout_gravity="center"/>

</LinearLayout>
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity
{
    EditText edit_name, edit_hobby;
    Button search_btn;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        edit_name = (EditText) findViewById(R.id.edit_name);
        edit_hobby = (EditText) findViewById(R.id.edit_hobby);
        search_btn = (Button) findViewById(R.id.search_btn);

        search_btn.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View view)
            {
                String name = edit_name.getText().toString();
                String hobby = edit_hobby.getText().toString();
                Intent intent = new Intent(MainActivity.this, OtherActivity.class);
                intent.putExtra("name", name);
                intent.putExtra("hobby", hobby);
                startActivity(intent);
            }
        });
    }
}

name, hobby를 각각 입력한 후 버튼을 누르면 다음 액티비티로 입력값을 갖고 이동할 뿐인 간단한 코드다.

이제 이동한 후의 액티비티를 코딩할 차례다. 액티비티명은 OtherActivity로 한다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".OtherActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="100dp"
        android:layout_marginStart="30dp"
        android:layout_marginEnd="30dp"
        android:weightSum="1">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".3"
            android:text="이름 : "
            android:textSize="30sp"
            android:gravity="end"/>

        <TextView
            android:id="@+id/get_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".7"
            android:textSize="30sp"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="100dp"
        android:layout_marginStart="30dp"
        android:layout_marginEnd="30dp"
        android:weightSum="1">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".3"
            android:text="취미 : "
            android:textSize="30sp"
            android:gravity="end"/>

        <TextView
            android:id="@+id/get_hobby"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight=".7"
            android:textSize="30sp"/>

    </LinearLayout>

    <Button
        android:id="@+id/back_activity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:text="뒤로 가기"
        android:textSize="20sp"
        android:layout_gravity="center"/>

</LinearLayout>
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class OtherActivity extends AppCompatActivity
{
    String name, hobby;
    TextView get_name, get_hobby;
    Button back_btn;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_other);

        get_name = (TextView) findViewById(R.id.get_name);
        get_hobby = (TextView) findViewById(R.id.get_hobby);
        back_btn = (Button) findViewById(R.id.back_activity);

        Intent intent = getIntent();
        name = intent.getStringExtra("name");
        hobby = intent.getStringExtra("hobby");
        Log.e("OtherActivity", "받아온 이름 : " + name + ", 취미 : " + hobby);

        back_btn.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View view)
            {
                finish();
            }
        });
    }
}

기초 공사는 끝났다. 이제 본격적으로 레트로핏을 사용해 서버와 통신해서, DB 안의 데이터를 가져다가 텍스트뷰를 바꿔보자.

먼저 레트로핏을 쓰려면 4가지 정도가 필요하다.

 

  • 웹 서버
  • 웹 서버 주소를 변수로 가진 클라이언트(ApiClient.java)
  • 웹 서버 안의 PHP 파일에 접근해 해당 파일의 쿼리문을 수행하는 함수들을 담은 인터페이스(ApiInterface.java)
  • 모델 클래스(리사이클러뷰를 만들 때 쓰는 그 모델 클래스다. POJO, DTO 등 부르는 말은 많은데 난 모델 클래스라고 부른다)

웹 서버와 PHP 파일은 여기까지 따라왔다면 모두 준비됐을 테니 생략한다. 앱에서 필요한 것은 인터페이스 파일, 클라이언트 파일, 모델 클래스 3가지와 액티비티, XML 파일 뿐이다.

인터페이스에 입력되는 내용은 GET/POST 중 어떤 방식인지, PHP 파일명은 무엇인지, 서버와 통신하는 Call 객체를 List 형으로 만들것인지 모델 클래스 형태로 만들 것인지 등을 정의한 추상 메서드들이다.

클라이언트 파일에는 내 경우 서버 주소와 레트로핏 객체, 레트로핏 객체가 아직 없을 경우 GSON을 적용시켜 레트로핏의 빌더 메서드를 통해 레트로핏 객체를 생성하는 static 메서드가 있다.

모델 클래스엔 name, hobby 필드와 이것들의 게터세터를 작성하는데, 이 때 어노테이션(@)을 사용해서 조금 특이하게 작성해야 한다.

 

레트로핏의 사용 순서는 대략 아래와 같다.

 

1. ApiClient의 static 메서드를 통해 레트로핏 객체를 만들고, 그것을 ApiInterface 객체에 매핑시킨다.

2. 그 다음 Call 객체를 생성하고 ApiInterface 객체를 참조해서 인터페이스 안에 작성된 추상 메서드를 호출한다. 이 때 추상 메서드에 필요한 인자가 있다면 만들든 초기화하든 해서 넣어줘야 한다.

3. 그 다음 Call 객체를 참조해서 enqueue() 콜백 메서드를 호출해 서버와의 통신이 성공/실패했을 때 어떻게 할지를 각각 코딩해준다.

 

이 포스팅 자체가 이렇게도 쓴다라는 걸 보여주는 하나의 예시일 뿐이니 이 방식이 무조건 정답은 아니다. 많고 많은 방식 중 하나일 뿐이니 일단 돌아가는 판을 이해하자.

먼저 모델 클래스부터 코딩한다.

name, hobby 2가지 값을 받아올 것이니 String으로 변수 2개를 생성한 다음 게터세터를 만들어주면 된다.

 

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

public class Person
{
    @Expose
    @SerializedName("name") private String name;

    @Expose
    @SerializedName("hobby") private String hobby;

    public String getName()
    {
        return name;
    }

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

    public String getHobby()
    {
        return hobby;
    }

    public void setHobby(String hobby)
    {
        this.hobby = hobby;
    }
}

 

위에서 말한대로 private String 앞에 희한한 것들이 붙어있다.

앞의 @는 어노테이션이라는 요소인데, @SerializedName 뒤의 괄호 안에는 컬럼명을 넣어준다. 변수명은 똑같이 지을 필요 없지만, 엔간해선 컬럼명과 똑같이 지어주는 게 덜 헷갈린다.

어노테이션이 뭔지, 각 어노테이션이 무슨 역할을 하는지 모른다면 각자 공부하자.

 

다음은 ApiInterface.java다.

 

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

public interface ApiInterface
{
    @GET("example_select.php")
    Call<Person> getNameHobby(
            @Query("name") String name,
            @Query("hobby") String hobby
    );
}

추상메서드의 리턴형을 정의하고 인자를 정했다. String name, hobby는 나중에 액티비티에서 넣어줄 것이다.

GET 방식으로 위에서 작성한 example_select.php 파일을 요청한다는 것도 보인다.

다음은 ApiClient.java다.

 

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ApiClient
{
    private static final String BASE_URL = "http://xx.xxx.xxx.xx/";
    private static Retrofit retrofit;

    public static Retrofit getApiClient()
    {
        Gson gson = new GsonBuilder()
                .setLenient()
                .create();

        if (retrofit == null)
        {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build();
        }

        return retrofit;
    }
}

앱 수준 gradle에 의존성 주입 시 3개를 다 넣었다면 addConverter 부분에서 빨간 줄이 나진 않을 것이다.

BASE_URL에 들어가는 문자열 맨 뒤에 '/'을 꼭 넣어주자.

이제 OtherActivity를 아래처럼 바꾸면 된다.

 

public class OtherActivity extends AppCompatActivity
{
    String name, hobby;
    TextView get_name, get_hobby;
    Button back_btn;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_other);

        get_name = (TextView) findViewById(R.id.get_name);
        get_hobby = (TextView) findViewById(R.id.get_hobby);
        back_btn = (Button) findViewById(R.id.back_activity);

        Intent intent = getIntent();
        name = intent.getStringExtra("name");
        hobby = intent.getStringExtra("hobby");
        Log.e("OtherActivity", "받아온 이름 : " + name + ", 취미 : " + hobby);

        // 레트로핏으로 서버에서 값을 받아온다
        getNameHobby(name, hobby);

        back_btn.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View view)
            {
                finish();
            }
        });
    }

    private void getNameHobby(String name, String hobby)
    {
        ApiInterface apiInterface = ApiClient.getApiClient().create(ApiInterface.class);
        Call<Person> call = apiInterface.getNameHobby(name, hobby);
        call.enqueue(new Callback<Person>()
        {
            @Override
            public void onResponse(@NonNull Call<Person> call, @NonNull Response<Person> response)
            {
                if (response.isSuccessful() && response.body() != null)
                {
                    String getted_name = response.body().getName();
                    String getted_hobby = response.body().getHobby();
                    Log.e("getNameHobby()", "서버에서 이름 : " + getted_name + ", 서버에서 받아온 취미 : " + getted_hobby);
                }
            }

            @Override
            public void onFailure(@NonNull Call<Person> call, @NonNull Throwable t)
            {
                Log.e("getNameHobby()", "에러 : " + t.getMessage());
            }
        });
    }
}

getNameHobby()와 버튼 클릭 리스너가 추가되었다. 클릭 리스너는 제끼고 getNameHobby()가 중요하니까 이 메서드를 보자.

위에서 레트로핏을 사용하는 순서를 간략하게 적었는데, 첫 번째 코드인 ApiInterface 어쩌고 부분이 바로 ApiInterface 객체에 레트로핏 객체를 매핑시키는 부분이다.

그 후 Call 객체를 모델 클래스(Person)에 맞춰 생성한 뒤, ApiInterface 객체를 참조해서 추상 메서드인 getNameHobby()를 호출한다. 그리고 메서드 선언부에서 String name, hobby를 인자로 넣을 것을 정의했다.

안의 onResponse()를 보면, if문으로 response의 상태를 확인한 다음 게터로 name, hobby 각각의 값들을 받아와 getted_name, getted_hobby 변수에 저장한다.

여기까지 됐다면 로그로 메인 액티비티에서 받아온 값과 서버에서 받아온 값을 출력해 확인해보자.

 

 

하드코딩하긴 했지만 DB에서 알맞은 값을 가져오는 걸 볼 수 있다.

그럼 이제 위의 PHP 파일에서 주석 친 부분을 지우고 제대로 써보자.

<?php

header("Content-type:application/json");

require_once 'example_con.php';

$name = $_GET['name'];
$hobby = $_GET['hobby'];

$sql = "SELECT * FROM person WHERE name = '$name' AND hobby = '$hobby'";

$result = mysqli_query($con, $sql);

$data = mysqli_num_rows($result);

if ($data > 0)
{
    $error = "ok";
    echo json_encode(array("response" => $error, "name" => $name, "hobby" => $hobby));
}
else
{
    $error = "failed";
    echo json_encode(array("response" => $error));
}

mysqli_close($con);

GET 방식으로 앱에서 데이터를 받으려면 $_GET['']을 써줘야 한다. POST라면 $_POST['']를 쓰면 된다.

그리고 $name, $hobby를 쿼리문에 삽입해 이 조건에 맞는 데이터를 가져오도록 한다.

이렇게 고친 다음 다시 앱을 확인해보자. 참고로 PHP 파일의 내용을 수정하기만 했을 땐 앱을 다시 빌드할 필요가 없이, 액티비티에 들어가서 확인하면 된다.

 

 

잘 가져오는 걸 볼 수 있다.

여기서 한 editText에 틀린 값을 넣으면 아래와 같은 로그를 볼 수 있다.

 

취미에서 book을 빼고 버튼을 눌렀더니 인텐트를 통해서 데이터는 제대로 OtherActivity로 왔지만, 서버에서는 아무것도 받아오지 못했다.

왜냐면 쿼리문에서 AND 키워드를 썼기 때문에 두 조건이 일치하는 것만 가져오기 때문이다. 하나라도 틀리면 얄짤없이 Null값을 반환한다.

 

여기까지가 레트로핏으로 서버에서 값을 가져오는 예시에 대한 포스팅이다.

다음 포스팅은 레트로핏으로 CRUD하는 예제를 포스팅하겠다.

반응형
Comments