안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

테이블에 데이터를 추가하는 스크립트는 다음과 같다. 샘플스크립트 여러개 올려두니 참고하자.

데이터를 추가하기 위해서는 INSERT문을 사용하게된다.

    public static void insertTable(SQLiteDatabase database, int gubunType, String sDate){

        if(database != null){
            try {
                String currentDateString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(simpleDateFormat.parse(currentDateString));
                //Log.e("hour", ""+calendar.get(Calendar.HOUR));
                //Log.e("minutes", ""+calendar.get(Calendar.MINUTE));
                //Log.e("seconds", ""+calendar.get(Calendar.SECOND));
                int hour = calendar.get(Calendar.HOUR);
                if(hour < 1) hour = 12;
                sDate = sDate + " " + DateUtil.getTime(hour) + ":" +  DateUtil.getTime(calendar.get(Calendar.MINUTE)) + ":" +  DateUtil.getTime(calendar.get(Calendar.SECOND));

                ULog.d("TAG", "=========== insertTable: 저장날짜 : " + sDate  + "     gubunType = " + gubunType );
//                DateFormat df1 = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
//                Date dt = df1.parse(sDate);
//
//                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//                String eventDate = sdf.format(dt);
//                ULog.d(DataBaseUtil.class.getSimpleName(), "=========== eventDate: " + eventDate + "        gubunType:" + gubunType);
                database.execSQL("INSERT INTO TB_FINEDUST_RECORD(_event_type, _event_date ) VALUES"
                        + "("
                        + gubunType + ","
                        + "'" + sDate + "'"
                        + ")");
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

샘플2

    public static void insertMyNumbers(SQLiteDatabase database,  String _event_date,  int _my_number_1, int _my_number_2, int _my_number_3, int _my_number_4, int _my_number_5, int _my_number_6, int _reg_type){

        //_reg_type 1 : 직접등록, _reg_type 2 : 일반번호 등록, _reg_type 3: 고급번호 등록
        if(database != null){
            try {
                database.execSQL("INSERT INTO TB_LOTTO_MY_NUMBER(_event_date, _my_number_1, _my_number_2, _my_number_3, _my_number_4, _my_number_5, _my_number_6, _is_delete , _reg_type ) VALUES"
                        + "("
                        + "'" + _event_date + "',"
                        + _my_number_1 + ","
                        + _my_number_2 + ","
                        + _my_number_3 + ","
                        + _my_number_4 + ","
                        + _my_number_5 + ","
                        + _my_number_6 + ","
                        + 1 + ","
                        + _reg_type
                        + ")");
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

샘플3

    public static void insertLottoWinerNumbers(SQLiteDatabase database, int _event_round, String _event_date,  String _winner_numbers, int _bonus_number){

        if(database != null){
            try {
                database.execSQL("INSERT INTO TB_LOTTO_WINNER_NUMBER(_event_round, _event_date, _winner_numbers, _bonus_number ) VALUES"
                        + "("
                        + _event_round + ","
                        + "'" + _event_date + "',"
                        + "'" + _winner_numbers + "',"
                        + _bonus_number
                        + ")");
            }catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

데이터베이스 초기화 방법은 아래 링크를 참고
1. 안드로이드 SQLite 사용을 위한 SQLiteOpenHelper 커스터마이징

2. 안드로이드 SQLite 테이블 생성 방법(CREATE TABLE)

3. 안드로이드 SQLite 데이터 삭제 방법(DELETE 문)

4. 안드로이드 SQLite 데이터 변경 방법(UPDATE문)

SQLite에 대해선 이전에 포스팅을 작성한 적이 있다.

onlyfor-me-blog.tistory.com/45

[Android] SQLite란? - 1 -

SQLite는 쉐어드, 룸 DB, Realm 따위와 같이 안드로이드에서 제공하는 앱 DB의 한 종류이다. 특이한 것이 있다면 이름 앞에 SQL이 붙는다는 점이다. SQL이 뭐냐면 structured query language로, 사전적 의미는.

onlyfor-me-blog.tistory.com

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

이 포스팅에선 SQLite를 좀 더 살펴본 다음, INSERT문을 사용해 DB에 데이터를 저장하는 예제를 살펴보려고 한다.

보통 데이터를 저장할 때는 웹 서버 같이 어떤 서버 안의 DB에 저장한다.

그런데 항상 서버의 DB에 모든 데이터들을 저장하기는 좀 그렇고 부담스럽기도 하다.

데이터 중에선 클라이언트에 저장해두고 싶은 데이터들도 있을 것이다. 그런데 안드로이드에 DB가 있나?

있다. 꽤 준수한 성능을 가져서 고급 개발(?)이거나 서버의 DB를 써야 할 정도로 많은 데이터를 다루는 게 아닌 이상, 제법 쓸만한 DB가 있다. 그것이 이 포스팅에서 설명할 SQLite 되시겠다.

그 전에 SQLite란 무엇일까?

SQLite란 이름을 보면 SQL이란 글자가 들어가 있다. SQL은 이전에 NoSQL과 SQL을 적어뒀던 포스팅에서 말한 SQL과 같은 것을 말한다.

이것의 읽는 방법은 에스큐엘라이트, 혹자는 에스큐라이트, 시퀄라이트라고도 읽는다. 뜻만 통하면 되니 읽는 법은 알아서 하자.

아무튼 이것에 대해서 영문 위키백과는 아래와 같이 말하고 있다.

SQLite는 C 라이브러리에 포함된 관계형 데이터베이스 관리 시스템(RDBMS)이다. 다른 데이터베이스 관리 시스템과 달리 SQLite는 클라이언트-서버 데이터베이스 엔진이 아니다. 오히려 최종 프로그램에 포함된다. SQLite는 ACID와 호환되며, 일반적으로 PostgreSQL 구문을 따르는 대부분의 SQL 구문을 구현한다. 그러나 SQLite는 도메인 무결성을 보장하지 않는 동적 및 약한 구문의 SQL 구문을 사용한다. SQLite는 적절한 경우 형식 간에 데이터를 변환하려고 시도한다. 이 경우 문자열 "123"은 정수로 변환되지만, 이런 변환이 불가능한 경우 데이터를 그대로 저장한다. SQLite는 웹 브라우저 같은 애플리케이션 소프트웨어의 로컬 / 클라이언트 스토리지를 위한 임베디드 데이터베이스 소프트웨어로 널리 사용된다...(중략)

설명이 많이 어렵고 이해가 안된다. 한글 위키백과에선 아래와 같이 말한다.

SQLite는 MySQL, PostgreSQL 같은 데이터베이스 관리 시스템이지만, 서버가 아니라 응용 프로그램에 넣어 사용하는 비교적 가벼운 DB다...(중략)...대규모 작업엔 부적합하지만 중소 규모라면 속도에 손색이 없다.
API는 단순히 라이브러리를 호출하는 것만 있으며, 데이터를 저장하는 데 하나의 파일만을 사용하는 것이 특징이다. 구글 안드로이드 OS에 기본 탑재된 DB기도 하다.

한글 위키백과의 내용들은 좀 이해가 가는 듯하다. 서버가 아니라 응용 프로그램에 넣어 쓰는 비교적 가벼운 DB라고 한다. 그리고 안드로이드 운영체제에 기본 탑재된 DB가 SQLite라고 한다.

근데 왜 Lite(라이트)라는 이름이 붙었을까? 보통 라이트라는 표현은 비교대상보다 용량이 좀 가볍거나, 성능이 몇 개 빠진 무언가를 말하는 뉘앙스를 가진 단어다. SQLite에서도 이런 의미가 통할까?

결론은 맞다. SQLite의 Lite는 설정, DB 관리 및 필요한 리소스 측면에서 가볍다는 것을 의미한다.(The lite in SQLite means lightweight in terms of setup, database administration, and required resources.)

그럼 이쯤해서 MySQL이나 MariaDB 따위의 DB를 써보지 않은 사람이라면 도대체 뭔 차이가 있는 건지 모를 수 있다. 어차피 다 비슷한 DB니까 그 나물에 그 밥인 거 아닌가 생각할 수 있다. 그러나 둘 사이에는 꽤 큰 차이가 있다.

일반적으로 MySQL 같은 RDBMS는 별도의 서버 프로세스가 작동해야 한다. DB 서버에 액세스하려는 응용 프로그램은 TCP/IP 프로토콜을 써서 요청(Request)을 보내고 응답(Response)을 받는다. 이걸 클라이언트-서버 아키텍처라고 한다.

아래 그림은 RDBMS 클라이언트-서버 아키텍처를 표현한 것이다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

지금 다루는 포스팅의 큰 주제는 안드로이드 앱이니까 앱을 클라이언트라 치고, 어떤 서버에 MySQL DB가 들어있다고 가정한다.

앱과 DB간에 데이터를 저장하고 수정하고 지지고 볶으려면 어떤 매개체가 필요하다. 앱에서 냅다 다이렉트로 DB의 데이터를 건드릴 수는 없다. 이 때 앱과 DB를 연결해주는 매개체가 레트로핏 따위의 네트워킹 라이브러리 및 웹 서버다.

앱에서 editText에 문자열을 입력한 뒤 이걸 서버에 저장하려면 레트로핏을 준비하고, INSERT문과 DB connection을 다루는 PHP 파일들을 작성한 뒤, 그 파일을 토대로 서버 안의 DB에 접근해 데이터를 추가하는 순서로 작업을 진행해야 한다. 참고로 중간에 레트로핏 사용과 PHP 파일 작성은 순서를 거꾸로 해도 무관하다. 취향 차이다.

본론으로 돌아와서, 아무튼 위와 같은 방식이 일반적인 클라이언트와 서버 사이에서 일어나는 데이터 이동의 형태다.

반면 SQLite는 위와 같이 작동하지 않는다. SQLite를 실행하는 데 서버 따윈 필요없다. SQLite DB는 DB에 접근하는 응용 프로그램과 통합된 형태다. 응용 프로그램(앱)은 SQLite DB와 상호작용해서 디스크에 저장된 DB 파일에서 직접 읽고 쓴다. 아래 그림은 SQLite Serverless 아키텍처를 보여주는 그림이다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

SQLite는 외부 서버를 쓰지 않고 안드로이드 OS 자체에 들어있는 DB를 쓰는 것이기 때문에, 준비만 적절하게 하면 다이렉트로 DB에 데이터를 저장하고 확인하고 지지고 볶을 수 있다.

그럼 간단하게 INSERT 문을 써서 데이터를 저장하는 예시를 확인해보자.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="30sp">

    <EditText
        android:id="@+id/title_input"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="68dp"
        android:layout_marginTop="148dp"
        android:ems="10"
        android:hint="Book title"
        android:inputType="textPersonName"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/author_input"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="100dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="100dp"
        android:ems="10"
        android:hint="Book author"
        android:inputType="textPersonName"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/title_input" />

    <EditText
        android:id="@+id/pages_input"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="101dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="101dp"
        android:ems="10"
        android:hint="Book pages"
        android:inputType="textPersonName"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/author_input" />

    <Button
        android:id="@+id/add_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="140dp"
        android:layout_marginTop="84dp"
        android:layout_marginEnd="136dp"
        android:text="데이터 추가"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.697"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/pages_input" />

</androidx.constraintlayout.widget.ConstraintLayout>
public class AddActivity extends AppCompatActivity
{
    EditText title_input, author_input, pages_input;
    Button add_button;

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

        title_input = findViewById(R.id.title_input);
        author_input = findViewById(R.id.author_input);
        pages_input = findViewById(R.id.pages_input);
        add_button = findViewById(R.id.add_button);
        add_button.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                MyDatabaseHelper myDb = new MyDatabaseHelper(AddActivity.this);
                myDb.addBook(title_input.getText().toString().trim(), author_input.getText().toString().trim(), Integer.parseInt(pages_input.getText().toString().trim()));
            }
        });
    }
}

화면은 대충 editText 3개랑 버튼 1개 두고 클래스 파일에선 버튼에 클릭 리스너 하나 단 뒤, MyDatabaseHelper란 클래스의 객체를 만들어 addBook()이란 메서드를 호출해 사용하고 있다.

이제 중요한 MyDatabaseHelper 클래스를 확인해보자.

import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.Toast;

import androidx.annotation.Nullable;

public class MyDatabaseHelper extends SQLiteOpenHelper
{
    private Context context;
    private static final String DATABASE_NAME = "Practice.db";
    private static final int DATABASE_VERSION = 1;
    private static final String TABLE_NAME = "practice_library";
    private static final String COLUMN_ID = "_id";
    private static final String COLUMN_TITLE = "book_title";
    private static final String COLUMN_AUTHOR = "book_author";
    private static final String COLUMN_PAGES = "book_pages";

    public MyDatabaseHelper(@Nullable Context context)
    {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
        this.context = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db)
    {
        String query = "CREATE TABLE " + TABLE_NAME
                + " (" + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                + COLUMN_TITLE + " TEXT, "
                + COLUMN_AUTHOR + " TEXT, "
                + COLUMN_PAGES + " INTEGER); ";
        db.execSQL(query);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
    {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
        onCreate(db);
    }

    void addBook(String title, String author, int pages)
    {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues cv = new ContentValues();

        cv.put(COLUMN_TITLE, title);
        cv.put(COLUMN_AUTHOR, author);
        cv.put(COLUMN_PAGES, pages);
        long result = db.insert(TABLE_NAME, null, cv);
        if (result == -1)
        {
            Toast.makeText(context, "Failed", Toast.LENGTH_SHORT).show();
        }
        else
        {
            Toast.makeText(context, "데이터 추가 성공", Toast.LENGTH_SHORT).show();
        }
    }

}

MyDatabaseHelper 클래스는 SQLiteOpenHelper라는 추상 클래스를 상속하고 있다. 그리고 각종 상수를 써서 어떤 값들을 정의해두고 있는 게 보인다. 상수들을 정리하면 아래와 같다.

  • DATABASE_NAME : SQLite DB의 이름으로 사용될 문자열이다. 나중에 DB를 확인해보면 저 이름으로 DB가 만들어져 있다.
  • TABLE_NAME : DB 안에 만들어지는 테이블의 이름으로 사용될 문자열이다. 데이터는 DB에 저장되는게 아니라 엄밀히 말하면 DB 안의 테이블의 컬럼에 저장된다. 이것에 대해선 후술한다.
  • COLUMN_ID, COLUMN_TITLE, COLUMN_AUTHOR, COLUMN_PAGES : 이 4개는 테이블 안에 생성될 컬럼들의 이름으로 사용될 문자열들이다. id는 테이블 안에 데이터들이 생길 때마다 붙는 순서표라고 생각하면 되고, 나머지는 문자열들과 정수값이 저장될 컬럼들이다.

처음에 SQLiteOpenHelper를 상속하면 메서드 2개를 implements할 수 있고 생성자 1개를 만들게 되는데, 그 생성자에서 context를 제외한 나머지 3개는 쳐낸다. 그리고 정의한 상수 2개와 null값 1개를 넣어준 뒤, this 키워드를 써서 액티비티의 context를 가져온다. 생성자를 만들 때 종류가 3개 나오는데 맨 처음 것을 선택한 다음 값들을 바꿔주면 된다.

onCreate()는 DB가 처음 생성될 때 호출되는 메서드다. 여기서 테이블 생성과 쿼리문을 인자로 받아 실행하는 execSQL()을 호출해 테이블 안에 데이터를 채우는 작업을 수행한다.

참고로 String query 안에서 스페이스 바를 빼먹고 안 쓰지 말자. 스페이스 바를 깜빡 잊고 안 쓰면 앱을 빌드할 때 로그캣에 에러가 뻥뻥 터지는 걸 볼 수 있다.

그리고 가로 일렬로 쭉 쓰는 것보다 저렇게 엔터를 필요한 곳에 쳐서 줄을 여러 개로 나누는 편이 내가 쓴 코드를 좀 더 확인하기 편할 수 있다.

그 밑의 onUpgrade()는 DB를 업그레이드해야 할 때 호출되는 메서드다. 업그레이드란 존재하는 DB 파일에 저장된 버전의 번호(DATABASE_VERSION)가 생성자에서 요청하는 것보다 낮은 경우, 기존의 DB를 삭제하고 다시 생성(onCreate)하는 걸 말한다. 그래서 onUpgrade() 안의 1번째 코드가 테이블을 삭제하는 DROP문을 사용하는 내용이고, 2번째 코드가 DB를 생성하는 onCreate()를 호출하는 내용이다.

그 밑의 addBook()은 3개의 인자를 받아서 SQLite DB에 인자들을 각 컬럼에 넣은 뒤 결과값을 if문으로 나눠서 -1이면 실패, -1이 아니면 성공이라는 토스트를 띄우는 메서드다.

메서드 안의 1번째 줄은 SQLiteDatabase 객체를 만든 뒤 이 객체를 쓰기(Write)가 가능하도록 설정한다는 내용이다. 이 처리를 해줘야 테이블에 데이터를 추가할 수 있다.

2번째 줄부터는 ContentValues라는 클래스의 객체를 생성한다. ContentValues란 addBook()에 들어오는 데이터를 저장하는 객체다. 그래서 cv라는 ContentValues 클래스의 객체를 통해 put()을 호출해서 title, author, pages 3개의 인자를 각각 알맞은 컬럼명에 저장하고 있다.

그 후 long 변수에 db 객체를 통한 insert()의 결과값을 저장한 다음, 이 값에 따라 DB 저장이 실패인지 성공인지를 토스트로 확인한다.

여기서 만들어진 addBook()은 액티비티의 onCreate()에서 호출되어 3개의 각 editText에서 문자열들을 가져와 DB 테이블의 각 컬럼에 저장한다.

완성된 예제의 작동화면은 아래와 같다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

데이터 추가 성공이라는 토스트가 나오는 걸 확인할 수 있다.

이제 만들어진 DB를 확인해보자. 확인하려면 어떤 프로그램을 설치해서 확인하는 방법이 있다.

바로 "SQLiteDatabaseBrowserPortable"이라는 프로그램이다. 설치는 아래 링크에서 자신의 컴퓨터 OS에 맞는 파일을 다운받아 설치하면 된다.

sqlitebrowser.org/

DB Browser for SQLite

DB Browser for SQLite The Official home of the DB Browser for SQLite Screenshot What it is DB Browser for SQLite (DB4S) is a high quality, visual, open source tool to create, design, and edit database files compatible with SQLite. DB4S is for users and dev

sqlitebrowser.org

설치한 지가 오래되서 위 링크에서 다운받은 프로그램이 한글판인지는 모르겠다.

한글판이라면 설치 후 프로그램을 실행할 시 아래 화면이 나온다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

이제 여기서 DB를 확인할 수 있는데, 확인하려면 당연히 DB 파일이 있어야 한다.

DB 파일을 얻으려면 아래 순서대로 하면 된다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

안드로이드 스튜디오의 우측 하단 or 상단을 보면 세로로 Device File Explorer라는 버튼이 있다. 이걸 누르고 data > data > 프로젝트 이름 타이핑 후 엔터 > databases > XXX.db와 그 하위 파일들을 모두 선택한 뒤 우클릭해서 Save as를 누르고, 바탕화면 등 원하는 위치에 저장시키면 된다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

내 경우는 databases 안에 2개의 파일들이 생성돼 있는데 3개가 있을 수도 있다. 아무튼 모든 파일들을 선택한 다음 우클릭해서 Save as하고 원하는 위치에 저장한다.

이제 위에서 설치했던 프로그램으로 돌아가서, 우측 상단의 데이터베이스 열기 버튼을 눌러 저장한 파일을 연다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

practice_library를 클릭하면(테이블 이름이다. 위에서 테이블 이름을 이 예제와 다르게 설정했다면 이름이 다를 수 있다) 내가 만든 컬럼들이 파일 안에 존재하는 걸 볼 수 있다. 위의 데이터 보기 탭을 누르면 아래와 같은 화면이 나온다.

안드로이드 SQLite INSERT INTO - andeuloideu SQLite INSERT INTO

다시 DB 파일을 다운받기 귀찮아서 기존에 다운받았던 파일을 재탕했지만, 데이터 보기 탭을 누르면 자신이 입력했던 데이터들이 가로로 나란히 줄지어져 있는 걸 볼 수 있다.

이 화면이 DB 안의 테이블 안에 생성된 컬럼들, 그 컬럼들에 각각 저장된 데이터들을 보여주는 화면이다.

처음엔 SQL문이 잘 이해가 안되고 SQLiteOpenHelper를 상속하는 DatabaseHelper 클래스의 작성이 어려워서 쓸 엄두가 안 날 수 있지만, 개발을 공부하다 보면 늦든 빠르든 언젠가는 써야 하는 DB와 SQL문이다.

SQLite를 공부하고 MySQL 같은 DB를 쓰던, 뭘 먼저 쓰던 하나에 익숙해지면 다른 걸 쓰는 건 그리 어렵지 않다. 형태와 쓰는 도구만 바뀌었을 뿐 SQL문을 써서 테이블과 컬럼을 만들고 원하는 데이터를 알맞은 자료형으로 저장한다는 본질적인 DB 사용법은 바뀌지 않으니 계속 연습하자. 물론 나도 엄청 많이 연습해야 한다.