1.  양방향 액티비티

메인 액티비티에서 세컨드 액티비티로 데이터를 넘긴 후에 세컨드 액티비티에서 메인 액티비티로 데이터를 돌려주는 경우

 

1)  메인 액티비티에서 putExtra()로 인텐트에 데이터를 넣는 것은 동일하지만, 세컨드 액티비티에서 데이터를 돌려 받으려면 액티비티를 호출할 때startActivityForResult() 메서드를 사용해야 함
2) 그리고 세컨드 액티비티에서 finish()로 끝내기 전에 메인 액티비티에 돌려줄 인텐트를 생성하여 putExtra()로 데이터를 넣은 다음 setResult()로 돌려줌.
3) 메인 액티비티에서는 onActivityResult() 메서드를 오버라이딩하고 오버라인딩된 메서드 안에서 getExtra()로 돌려 받은 데이터를 사용.

 

 

 

activity_main.xml
<?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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editNum1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/editNum2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btnNewActivity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="더하기" />
    
</LinearLayout>

 

activity_second.xml
<?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"
    tools:context=".SecondActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnReturn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="돌아가기" />

</LinearLayout>

 

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = "메인 액티비티"

        val editNum1 = findViewById<EditText>(R.id.editNum1)
        val editNum2 = findViewById<EditText>(R.id.editNum2)
        val btnNewActivity = findViewById<Button>(R.id.btnNewActivity)
        btnNewActivity.setOnClickListener {
            val intent = Intent(applicationContext, SecondActivity::class.java)
            intent.putExtra("num1", editNum1.text.toString().toInt())
            intent.putExtra("num2", editNum2.text.toString().toInt())
            startActivityForResult(intent, 0)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) {
            val sum = data!!.getIntExtra("sum", 0)
            Toast.makeText(applicationContext, "합계 :  ${sum}", Toast.LENGTH_SHORT).show()
        }
    }
}

 

  📍  startActivityForResult(intent, 0);

       값을 돌려받기 위해 startActivityForResult()를 사용. 두 번째 파라미터에는 돌려받을 값이 있는 경우에 0이상을 사용.
       여러 액티비티를 쓰는 경우, 어떤 Activity인지 식별하는 값.

  📍  onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
        세컨드 액티비티에서 setResult()로 값을 돌려주면 오버라이딩한 onActivityResult() 메서드가 실행

   📍  if (resultCode == RESULT_OK) 
         setResult()에서 보낸 값이 RESULT_OK이면 인텐트에서 돌려받은 값을 토스트 메시지로 화면에 출력

 

SecondActivity.kt
class SecondActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        title = "Second 액티비티"

        // 인텐트 관련 처리
        val intent = intent
        val sum = intent.getIntExtra("num1", 0) + intent.getIntExtra("num2", 0)

        val btnReturn = findViewById<Button>(R.id.btnReturn)
        btnReturn.setOnClickListener {
            val intentResult = Intent(applicationContext, MainActivity::class.java)
            intentResult.putExtra("sum", sum)
            setResult(Activity.RESULT_OK, intentResult)
            finish()
        }
    }
}

 

  📍 val intent = intent
        val sum = intent.getIntExtra("num1", 0) + intent.getIntExtra("num2", 0)

 

      메인 액티비티로부터 받은 두 값을 더함

  📍 val intentResult = Intent(applicationContext, MainActivity::class.java)
       intentResult.putExtra("sum", sum)
       setResult(Activity.RESULT_OK, intentResult)

      setResult()로 메인 액티비티에 돌려줌. 메인 액티비티의 onActivityResult() 메서드가 실행

 

 

 


2.  암시적 인텐트

명시적 인텐트의 개념이 두 액티비티를 사용자가 직접 생성하고 코딩하는 것이라면, 암시적 인텐트 implicit intent는 약속된 액션을 지정하여 '안드로이드에서 제공하는 기존 응용 프로그램을 실행하는 것'
  예를 들어 전화번호를 인텐트로 넘긴 후에 전화 걸기 응용 프로그램이 실행되는 것.

 

  ⚡️  메인 액티비티에서 인텐트를 생성할 때 실행하고자 하는 액션을 지정하고 액션의 데이터 값을 설정하면 기존의 안드로이드 응용 프로그램이 실행됨

   

119에 응급 전화를 거는 형식의 예
Intent intent = new Intent(Intent.ACTION_DIAL, Url.parse("tel:/119"));
startActivity(intent);

 

전화 걸기와 구글 맵을 사용하려면 AndroidManifest.xml의 <application 위에 다음과 같이 권한을 추가
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"/>

 

암시적 인텐트의 XML 파일
<?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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnDial"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="전화 걸기" />

    <Button
        android:id="@+id/btnWeb"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="홈 페이지 열기" />

    <Button
        android:id="@+id/btnGoogle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="구글 맵 열기" />

    <Button
        android:id="@+id/btnSearch"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="구글 검색하기" />

    <Button
        android:id="@+id/btnSms"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="문자 보내기" />

    <Button
        android:id="@+id/btnPhoto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="사진 찍기" />

</LinearLayout>

 

암시적 인텐트의 Kotlin 코드
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = "암시적 인텐트 예제"
        val btnDial = findViewById<Button>(R.id.btnDial)
        val btnWeb = findViewById<Button>(R.id.btnWeb)
        val btnGoogle = findViewById<Button>(R.id.btnGoogle)
        val btnSearch = findViewById<Button>(R.id.btnSearch)
        val btnSms = findViewById<Button>(R.id.btnSms)
        val btnPhoto = findViewById<Button>(R.id.btnPhoto)

        btnDial.setOnClickListener {
            val tel = Uri.parse("tel:010-1234-5678")
            startActivity(Intent(Intent.ACTION_DIAL, tel))
        }

        btnWeb.setOnClickListener {
            val uri = Uri.parse("http://daum.net")
            startActivity(Intent(Intent.ACTION_VIEW, uri))
        }

        btnGoogle.setOnClickListener {
            val uri = Uri.parse("https://maps.google.com/maps?q="
                + 35.886606 + "," + 128.5938 + "&z=15" )
            startActivity(Intent(Intent.ACTION_VIEW, uri))
        }

        btnSearch.setOnClickListener {
            val intent = Intent(Intent.ACTION_WEB_SEARCH)
            intent.putExtra(SearchManager.QUERY, "안드로이드")
            startActivity(intent)
        }

        btnSms.setOnClickListener {
            val intent = Intent(Intent.ACTION_SENDTO)
            intent.putExtra("sms_body", "안녕하세요?")
            intent.setData(Uri.parse("smsto:010-1234-5678"))
            startActivity(intent)
        }

        btnPhoto.setOnClickListener {
            startActivity(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
        }

    }
}

 

  📍  val uri = Uri.parse("tel:010-1234-5678")
        startActivity(Intent(Intent.ACTION_DIAL, uri))

      전화를 걸기 위해 URI 문자열을 'tel:전화번호'형식으로 사용. 액션으로 ACTION_DIAL을 사용하면 전화 걸기 창이 열림.

  📍  val uri = Uri.parse("http://daum.net")
        startActivity(Intent(Intent.ACTION_VIEW, uri))

       웹브라우저를 열기 위해 URI 문자열을 'http://웹 주소'형식으로 사용. 액션은 ACTION_VIEW를 사용.

  📍  val uri = Uri.parse("https://maps.google.com/maps?q=" + 35.86606 + "," + 128.5938 + "&z=15" )
        startActivity(Intent(Intent.ACTION_VIEW, uri))

      구글 맵을 열기 위해 URI 문자열을 구글 맵 주소과 경위도 형식으로 사용. 액션은 ACTION_VIEW를 사용

  📍  val intent = Intent(Intent.ACTION_WEB_SEARCH)
        intent.putExtra(SearchManager.QUERY, "안드로이드")

       구글 검색을 열기 위해 액션은 ACTION_WEB_SEARCH를 사용. 검색을 위해 putExtra()로 넘기는데, 첫 번째 파라미터로 SearchManager.QUERY를 사용하고 두 번째 파라미터에는 검색할 단어를 넘김.

  📍  val intent = Intent(Intent.ACTION_SENDTO)
         intent.putExtra("sms_body", "안녕하세요?")

       문자 메시지를 보내기 위해 액션은 ACTION_SENDTO를 사용. 보낼 문자는 putExtra()로 넘기는데, 첫 번째 파라미터에는 'sms_body'를 넣고 두 번째 파라미터에 보낼 문자를 넣음. setData()도 설정해야 함.

  📍  startActivity(Intent(MediaStore.ACTION_IMAGE_CAPTURE))

      카메라를 열기 위해 액션은 MediaStore.ACTION_IMAGE_CAPTURE를 사용.

 

 

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  activity_main.xml

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

    <LinearLayout
        android:id="@+id/layout01"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3">

        <ImageView
            android:id="@+id/imageView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic1" />

        <ImageView
            android:id="@+id/imageView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic2" />

        <ImageView
            android:id="@+id/imageView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic3" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/layout02"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3">

        <ImageView
            android:id="@+id/imageView4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic4" />

        <ImageView
            android:id="@+id/imageView5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic5" />

        <ImageView
            android:id="@+id/imageView6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic6" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/layout03"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="3">

        <ImageView
            android:id="@+id/imageView7"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic7" />

        <ImageView
            android:id="@+id/imageView8"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic8" />

        <ImageView
            android:id="@+id/imageView9"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:layout_weight="1"
            android:src="@drawable/pic9" />

    </LinearLayout>

    <Button
        android:id="@+id/btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="투표 종료" />

</LinearLayout>

 

  • 바깥 리니어레이아웃 안에 리니어레이아웃 3개, 버튼 1개를 생성하고 layout_weight를 3:3:3:1로 함
  • 3개의 레이아웃에 각각 3개의 이미지뷰를 넣음. 각 이미지뷰의 layout_weight는 1:1:1로 함. 필요에 따라 이미지뷰에 적당한 layout_margin을 적용. (예 : 5dp)
  • 이미지뷰 9개의 아이디는 imageView1 ~ imageView9, 버튼의 아이디는 btnResult.


2.  result.xml

서브 액티비티에서 사용할 result.xml을 res - layout 폴더에 생성

바깥은 테이블레이아웃으로 설정하고 stretchColumns 속성을 0으로 함

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:stretchColumns="0">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="15dp"  />

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <TableRow>

        <TextView
            android:id="@+id/textView1"
            android:layout_gravity="center_vertical"
            android:text="그림1"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar1"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView2"
            android:layout_gravity="center_vertical"
            android:text="그림2"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar2"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView3"
            android:layout_gravity="center_vertical"
            android:text="그림3"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar3"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView4"
            android:layout_gravity="center_vertical"
            android:text="그림4"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar4"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView5"
            android:layout_gravity="center_vertical"
            android:text="그림5"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar5"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView6"
            android:layout_gravity="center_vertical"
            android:text="그림6"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar6"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView7"
            android:layout_gravity="center_vertical"
            android:text="그림7"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar7"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView8"
            android:layout_gravity="center_vertical"
            android:text="그림8"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar8"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <TextView
            android:id="@+id/textView9"
            android:layout_gravity="center_vertical"
            android:text="그림9"
            android:textSize="15dp" />

        <RatingBar
            android:id="@+id/ratingBar9"
            style="?android:attr/ratingBarStyleIndicator"
            android:layout_gravity="right" />
    </TableRow>

    <TableRow>

        <Button
            android:id="@+id/btnReturn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_span="2"
            android:text="돌아가기" />
    </TableRow>
</TableLayout>

 

  • 각 <TableRow>에는 텍스트뷰 1개, 레이팅바 1개를 생성.
  • 마지막 <TableRow>에는 '돌아가기'를 생성.
  • 텍스트뷰의 아이디는 textView1 ~ textView9, 레이팅바의 아이디는 ratingBar1 ~ ratingBar9, 버튼의 아이디는 btnReturn.


3.  Kotlin 코드 작성 및 수정

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.layout_01)

        title = "명화 선호도 투표"

        // 그림을 클릭할 때마다 투표수를 저장할 9개짜리 배열을 선언하고 0으로 초기화
        val voteCounts = IntArray(9)

        // 이미지뷰 위젯을 저장할 9개짜리 배열을 선언
        val images = arrayOfNulls<ImageView>(9)

        // 이미지 버튼 Id 배열
        val imageIds = arrayOf(R.id.imageView1, R.id.imageView2, R.id.imageView3,
            R.id.imageView4, R.id.imageView5, R.id.imageView6,
            R.id.imageView7, R.id.imageView8, R.id.imageView9)

        // 이미지 이름 문자열 배열
        val imageNames = arrayOf("독서하는 소녀", "꽃장식 모자 소녀", "부채를 든 소녀",
            "이레느깡 단 베르양", "잠자는 소녀", "테라스의 두 자매",
            "피아노 레슨", "피아노 앞의 소녀들", "해변에서")

        // 각 이미지뷰에 대한 클릭 이벤트리스너를 생성. 이미지뷰가 9개나 되므로 반복문을 사용
        for (i in imageIds.indices) {
            images[i] = findViewById(imageIds[i]
            images[i]!!.setOnClickListener {
                voteCounts[i]++  // 그림을 클릭하면 해당 그림의 투표수가 증가
                Toast.makeText( // 그림을 클릭할 때마다 그림의 제목과 누적된 투표수를 토스트 메시지로 보여줌
                    applicationContext,
                    "${imageNames[i]} : 총${voteCounts[i]} 표",
                    Toast.LENGTH_SHORT
                ).show()
            }
        }

        // '투표 종료'에 대해 클릭 이벤트 리스너를 생성
        val btnResult = findViewById<Button>(R.id.btn)
        // 인텐트를 생성하고 인텐트에 투표수 배열과 그림 제목 배열을 넣은 후 결과 액티비티를 호출
        btnResult.setOnClickListener {
            val intent = Intent(applicationContext, ResultActivity::class.java)
            intent.putExtra("voteCounts", voteCounts)
            intent.putExtra("imageNames", imageNames)
            startActivity(intent)
        }

    }
}

 

intent.putExtra("voteCounts", voteCounts)
intent.putExtra("imageNames", imageNames)


  정수형 voteCount 배열을 VoteCounts라는 이름으로 넘김. 이것을 액티비티에서는 getIntArrayExtra() 메서드로 받음.
  문자열 배열인 imageNames을 지정한 imageNames은 getStringArrayExtra() 메서드로 받음.

 

ResultActivity.kt
class ResultActivity: AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.result)

        // MainActivity에서 보낸 투표 결과 값을 받음
        title = "투표결과"
        
        val intent = intent
        val voteCounts = intent.getIntArrayExtra("voteCounts")
        val imageNames = intent.getStringArrayExtra("imageNames")

        // 가장 많은 투표를 받은 이미지와 제목을 나타내는 변수
        val mainText = findViewById<TextView>(R.id.textView)
        val mainImg = findViewById<ImageView>(R.id.imageView)

        // 9개의 TextView, RatingBar 객체 배열
        val ratingBars = arrayOfNulls<RatingBar>(9)
        val textViews = arrayOfNulls<TextView>(9)

        // 9개의 TextView, RatingBar id 배열
        val textViewIds = arrayOf(R.id.textView1, R.id.textView2, R.id.textView3,
                      R.id.textView4, R.id.textView5, R.id.textView6,
                      R.id.textView7, R.id.textView8, R.id.textView9)

        val ratingBarIds = arrayOf(R.id.ratingBar1, R.id.ratingBar2, R.id.ratingBar3,
                        R.id.ratingBar4, R.id.ratingBar5, R.id.ratingBar6,
                        R.id.ratingBar7, R.id.ratingBar8, R.id.ratingBar9)

        val imageIds = arrayOf(R.drawable.pic1, R.drawable.pic2, R.drawable.pic3,
                       R.drawable.pic4, R.drawable.pic5, R.drawable.pic6,
                       R.drawable.pic7, R.drawable.pic8, R.drawable.pic9)

        var maxVotesIndex = 0
        for (i in 1 until voteCounts!!.size) {
            if (voteCounts[i] > voteCounts[maxVotesIndex]) {
                maxVotesIndex = i
            }

            mainText.text = imageNames!![maxVotesIndex]
            mainImg.setImageResource(imageIds[maxVotesIndex])
        }

        for (i in voteCounts!!.indices) {

            textViews[i] = findViewById(textViewIds[i])
            textViews[i]!!.text = imageNames!![i]

            ratingBars[i] = findViewById(ratingBarIds[i])
            ratingBars[i]!!.rating = voteCounts[i].toFloat()
            ratingBars[i]!!.stepSize = 1f

        }

        // 버튼을 클릭하면 서브 액티비티를 종료. 메인 액티비티로 돌아감.
        val btnReturn = findViewById<Button>(R.id.btnReturn)
        btnReturn.setOnClickListener {
            finish()
        }

    }
}

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  안드로이드의 4대 컴포넌트

안드로이드의 4대 컴포넌트는 액티비티 · 서비스 · 브로드캐스트 리시버 · 콘텐트 프로바이더


1)  액티비티 Activity

 화면을 구성하는 가장 기본적인 컴포넌트

 

2)  서비스 service

  눈에 보이는 화면(액티비티)과 상관없이 백그라운드에서 동작하는 컴퍼넌트. 백신 프로그램처럼 눈에 보이지는 않지만 계속 동작
  -  로컬에서 동작하는 서비스는 다음과 같이 세 단계를 거침
      서비스 생성 ▶️ 서비스 시작 ▶️ 서비스 종료

 

3)  브로드캐스트 리시버

  -  안드로이드는 여러 응용 프로그램이나 장치에 메시지를 전달하기 위해 방송 broadcasting 메시지를 사용
  -  안드로이드는 문자 메시지 도착, 배터리 방전, SD 카드 탈부착, 네트워크 환경 변화가 발생하면 전체 응용 프로그램이 알 수 있도록 방송 신호를 보냄 그리고 브로드캐스트 리시버 Broadcast Receiver는 이러한 방송 신호가 발생하면 반응함

 

4)  콘텐트 프로바이더

 콘텐트 프로바이더 Content Provider는 응용 프로그램 사이에 데이터를 공유하기 위한 컴포넌트
  -  안드로이드 응용 프로그램은 데이터에 자신만 접근할 수 있으므로 자신의 데이터를 외부에 공유하려면 콘텐트 프로바이더를 만들어야 함. 콘텐트 프로바이더에서 처리된 데이터는 일반적으로 데이터베이스 또는 파일로 저장

 


2.  액티비티의 개요

액티비티는 안드로이드폰에 나타나는 화면 하나하나를 말함. 액티비티는 사용자에게 보여주는 화면을 만들기 때문에 안드로이드의 4개 컴포넌트 중 가장 핵심적인 요소.

 

  • 안드로이드 프로젝트를 생성할 때 activity_main.xml과 MainActivity.kt 파일이 생성
  • activity_main.xml은 화면을 구성하는 코드로 되어 있지만 activity_main.xml이 아니라 MainActivity.kt가 액티비티에 해당
  • 일반적으로 액티비티 하나당 XML 파일 하나를 만들어서 사용
activity_main.xml 
<?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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/btnNewActivity"
        android:text="새 화면 열기" />


</LinearLayout>

 

second.xml 

 

    [res] - [layout] 에 second.xml 생성

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#CDE4F6">

    <Button
        android:id="@+id/btnReturn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="돌아가기" />

</LinearLayout>

 

SecondActivity.kt
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.second)

        val btnReturn: Button = findViewById(R.id.btnReturn)
        btnReturn.setOnClickListener {
            finish()
        }

    }
}

 

  📍  finish()가 호출되면 현재 액티비티를 종료. 세컨드 액티비티는 메인 액티비티에서 호출할 것이므로 세컨드 액티비티를 종료하면 다시 메인 액티비티가 나타남

 

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val btnNewActivity = findViewById<Button>(R.id.btnNewActivity)
        btnNewActivity.setOnClickListener {
            val intent = Intent(applicationContext, SecondActivity::class.java)
            startActivity(intent)
        }
    }
}

 

val intent = Intent(applicationContext, SecondActivity::class.java)

 

  -  인텐트를 생성. Intent의 첫 번째 파라미터 applicationContext는 메인 클래스의 컨텍스트를 반환
  -  applicationContext 대신 this@MainActivity 도 가능
  -  두 번째 파라미터로 이 인텐트에 포함될 액티비티 SecondActivity를 넘겨줌. ::class.java를 붙여야 한다는 것을 주의.

 

startActivity(intent)


  -  startActivity() : 새로운 액티비티를 화면에 출력. 파라미터로 인텐트를 받음.

 

AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
         category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".SecondActivity" />

 

  📍  안드로이드는 사용될 액티비티를 AndroidManifest.xml에 반드시 등록. 프로젝트를 생성할 때 메인 액티비티는 자동으로 등록되지만, 추가한 세컨드 액티비티는 별도로 등록해야 함.

 


3.  명시적 인텐트

인텐트 Intent는 안드로이드의 4대 컴포넌트가 서로 데이터를 주고받기 위한 메시지 객체.

명시적 인텐트와 암시적 인텐트로 구분


1)  명시적 인텐트와 데이터 전달

  🐰  명시적 인텐트 explicit intent는 다른 액티비티의 이름을 명확히 지정할 때 사용

val intent = Intent(applicationContext, SecondActivity::class.java)
startActivity(intent)


  ✓  Intent() 생성자의 두 번째 파라미터에서는 액티비티 클래스를 넘길 수 있는데, 위의 코드에서는 그 전에 생성한 SecondActivity로 정확히 지정. 그리고 생성한 인텐트를 넘겨서 세컨드 액티비티를 실행

  ✓  이처럼 명확하게 액티비티의 이름을 지정했기 때문에 명시적 인텐트가 됨. 일반적으로 명시적 인텐트는 사용자가 새로운 액티비티를 직접 생성하고 호출할 때 사용

  🐰  인텐트에 데이터를 담아서 넘기는 것도 가능. 메인 액티비티에서 인텐트에 데이터를 실어서 넘긴 다음 세컨드 액티비티에게 받은 데이터를 처리

 

 

1)  putExtra()를 이용해서 필요한 만큼 데이터를 인텐트에 넣음
2)  startActivity()로 인텐트를 다른 액티비티에 넘김
3)  인텐트를 받은 액티비티에서 getStringExtra(), getIntExtra(), getBooleanExtra() 등의 메서드로 넘어온 데이터에 접근

 

 

 


2)  레이팅바

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

    <RatingBar
        android:id="@+id/ratingBar1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <RatingBar
        android:id="@+id/ratingBar2"
        style="?android:attr/ratingBarStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:numStars="10"
        android:stepSize="1" />

    <RatingBar
        android:id="@+id/ratingBar3"
        style="?android:attr/ratingBarStyleIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:rating="1.5" />

    <Button
        android:id="@+id/btnIncrease"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="증가시키기" />

    <Button
        android:id="@+id/btnDecrease"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="감소시키기" />

</LinearLayout>

 

style="?android:attr/ratingBarStyleSmall"
style="?android:attr/ratingBarStyleIndicator"

 

  -  레이팅바의 스타일을 지정. 작은 모양과 중간 모양을 표시.

android:stepSize="1"


  -  한 번에 증가될 크기를 별 1개로 지정. 소수점으로 설정할 수 있으며 디폴트는 0.5.

android:numStars="10"


  -  별의 개수를 지정. 디폴트는 5개.

 

android:rating="1.5"


  -  초깃값을 설정. 1.5개가 채워진 상태로 표현.

 

kotlin 코드
val ratingBar1 = findViewById<RatingBar>(R.id.raingBar1)
val ratingBar2 = findViewById<RatingBar>(R.id.raingBar1)
val ratingBar3 = findViewById<RatingBar>(R.id.raingBar1)
val btnIncrease = findViewById<RatingBar>(R.id.btnIncrease)
val btnDecrease = findViewById<RatingBar>(R.id.btnDecrease)

btnDecrease.setOnClickListener {
    ratingBar1.rating = ratingBar1.rating + ratingBar1.stepSize
    ratingBar2.rating = ratingBar2.rating + ratingBar2.stepSize
    ratingBar3.rating = ratingBar3.rating + ratingBar3.stepSize
}    

btnIncrease.setOnClickListener {
    ratingBar1.rating = ratingBar1.rating + ratingBar1.stepSize
    ratingBar2.rating = ratingBar2.rating + ratingBar2.stepSize
    ratingBar3.rating = ratingBar3.rating + ratingBar3.stepSize
}

 

  -  rating으로 레이팅바에 채워진 개수와 stepSize로 레이팅바에 설정된 stepSize 속성의 증가 크기 (디폴트는 0.5)를 더하거나 뺌

 

 

 

 

[ 내용참고 : IT 학원 강의 ]

 


1.  Thymeleaf

스프링과 마찬가지로 스프링 부트도 다양한 뷰 view 관련 기술을 적용할 수 있음. 스프링은 대부분 JSP를 위주로 개발하는 경우가 많지만 스프링 부트는 Thymeleaf라는 템플릿 엔진을 주로 이용.

Thymeleaf는 '템플릿'이기 때문에 JSP 처럼 직접 데이터를 생성하지 않고, 만들어진 결과에 데이터를 맞춰서 보여주는 방식으로 구현.
JSP와 마찬가지로 서버에서 동작하기는 하지만 Thymleaf는 HTML을 기반으로 화면을 구성하기 때문에 HTML에 조금 더 가까운 방식으로 작성


2.   Thymeleaf 기초 문법

Thymeleaf는 JSP를 대신하는 목적으로 작성된 라이브러리이므로, JSP에서 필요한 기능들을 Thymeleaf로 구성


1) 인텔리제이 설정

앞에서 작성한 hello.html을 열기. Thymeleaf를 이용하기 위해서 가장 중요한 설정은 네임스페이스 xmlns에 Thymeleaf를 지정. 네임스페이스를 지정하면 'th:'와 같은 Thymeleaf의 모든 기능을 사용할 수 있음.

작성된 hello.html은 다음과 같이 'th:'로 시작하는 기능을 사용할 수 있지만 Model에 담긴 데이터를 사용할 때는 '해당 변수를 찾을 수 없다'는 방식으로 에러가 날 수 있음.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${msg}"></h1>
</body>
</html>


  📍  만일 에러가 발생하는 경우에는 인텔리제이의 설정을 조금 변경해서 에러 없는 화면을 보는 것이 더 좋기 때문에 Setting 메뉴에서 Thymeleaf를 검색하고 Unresolved references .. 를 체크 해제 해 주도록 함

    ➡️  설정을 변경하고 기존에 열려있는 hello.html 에디터를 종료한 후에 다시 에디터로 보면 변수에 대한 검사를 하지 않는 것을 확인


2)  Thymleaf 출력

Model로 전달된 데이터를 출력하기 위해서 HTML 태그 내에 'th:'로 시작하는 속성을 이용하거나 inlining을 이용
SampleController에 ex1()을 추가해서 '/ex/ex1'이라는 경로를 호출할 때 동작하도록 구성

 @GetMapping("/ex/ex1")
    public void ex1(Model model) {
        List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

        model.addAttribute("list", list);
    }

 

ex1()의 결과 화면은 templates 내에 ex 디렉토리를 생성하고 ex1.html을 추가
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- inlining 사용 -->
<h4>[[${list}]]</h4>
<hr>
<!-- 'th:' 속성 사용 -->
<h4 th:text="${list}"></h4>
</body>
</html>


 

3)  Thymeleaf 주석 처리

Thymeleaf가 작성하는 단계에서는 단순해 보이지만 디버깅할 때는 상황이 다름. 에러가 발생하게 되면 에러의 원인을 찾아내기 힘들다.
에러가 난 부분을 찾기 위해서는 주석 처리를 해야할 때는 '<!--/* ... */-->'를 이용하는 것이 좋음. 주석은 Thymeleaf가 파싱 처리할 때 삭제되어 처리되기 때문에 1) 잘못된 문법 에 대한 체크도 건너 뛸 수 있고, 삭제된 상태에서 처리되므로 2)브라우저에서는 아예 해당 부분은 결과 자체가 없음

 

hello.html 수정

 


 

4)  th:with를 이용한 변수 선언

Thymeleaf를 이용하는 과정에서 임시로 변수를 선언해야 하는 상황에서는 'th:with'를 이용해서 간단히 처리 가능
'th:with'로 만드는 변수를 '변수명 = 값'의 형태로 ', '를 이용해서 여러 개를 선언 할 수도 있음

    <div th:with="num1 = ${10}, num2 = ${20}">
        <h4 th:text="${num1 + num2}"></h4>
    </div>

 


3.  반복문과 제어문 처리

화면 구성에서 가장 많이 사용되는 반복문과 제어문 처리.
SampleController의 ex1()에서는 Model을 이용해 'List<String>'을 담고 ex1.html을 이용해서 출력하도록 구성.

 @GetMapping("/ex/ex1")
    public void ex1(Model model) {
        List<String> list = Arrays.asList("AAA", "BBB", "CCC", "DDD");

        model.addAttribute("list", list);
    }

 

반복문 처리는 크게 2가지 방법을 이용


    -  반복이 필요한 태그에 'th:each'를 적용하는 방법
    -  <th:block>이라는 별도의 태그를 이용하는 방법


    💡  'th:each' 속성을 이용할 때는 기존의 HTML을 그대로 둔 상태에서 반복 처리를 할 수 있다는 장점이 있지만 JSTL 과는 조금 이질적인 형태이고, <th:block>을 이용할 때는 추가로 태그가 들어가는 단점이 있음.

 

ex1.html의 내용을 변경
<ul>
    <li th:each="str: ${list}" th:text="${str}"></li>
</ul>

<ul>
    <th:block th:each="str: ${list}">
        <li>[[${str}]]</li>
    </th:block>
</ul>


 

1)  반복문의 status 변수

Thymeleaf는 th:each를 처리할 때 현재 반복문의 내부 상태에 변수를 추가해서 사용할 수 있음.
일명 status 변수라고 하는데 index / count / size / first / last / odd / even 등을 이용해서 자주 사용하는 값들을 출력할 수 있음

<ul>
    <li th:each="str, status: ${list}">
        [[${status.index}]] -- [[${str}]]
    </li>
</ul>

 

  ✓  status 변수명은 사용자가 지정할 수 있고, index는 0부터 시작하는 번호를 의미. count는 1부터 시작


 

2)  th:if / th:unless / th:switch

Thymeleaf는 제어문의 형태로 th:if / th:unless / th:switch를 이용할 수 있음

th:if / th:unless는 별도의 속성으로 사용할 수 있으므로 if ~ else 와는 다르게 사용
예를 들어 반복문의 홀수 / 짝수를 구분해서 처리하고 싶다면 다음과 작성

<ul>
    <li th:each="str, status: ${list}">
        <span th:if="${status.odd}">ODD -- [[${str}]]</span>
        <span th:unless="${status.odd}">EVEN -- [[${str}]]</span>
    </li>
</ul>


 

📍  ?를 이용하면 앞선 방식보다는 좀 더 편하게 이항 혹은 삼항 처리가 가능
      예를 들어 반복 중에 홀수 번째만 무언가를 보여주고 싶다면 다음과 같이 ? 뒤에 하나만 표현식을 사용할 수 있음

<ul>
    <li th:each="str, status: ${list}">
        <span th:text="${status.odd} ? 'ODD ---' + ${str}"></span>
    </li>
</ul>


 

📍  ?를 삼항연산자 그대로 사용할 수도 있음

<ul>
    <li th:each="str, status: ${list}">
        <span th:text="${status.odd} ? 'ODD ---' + ${str} : 'EVEN ---' + ${str}"></span>
    </li>
</ul>


 

📍 th:switch는 th:case와 같이 사용해서 Switch 문을 처리할 때 사용할 수 있음

<ul>
    <li th:each="str, status: ${list}">
        <th:block th:switch="${status.index % 3}">
            <span th:case="0">0</span>
            <span th:case="1">1</span>
            <span th:case="2">2</span>
        </th:block>
    </li>
</ul>


4.  Thymeleaf 링크 처리

  -  Thymeleaf는 '@'로 링크를 작성하기만 하면 됨

<a th:href="@{/hello}">Go to /hello </a>

 

 

1) 링크의 쿼리 스트링 처리

 

링크를 'key=value'의 형태로 필요한 파라미터를 처리해야 할 때 상당히 편리.

쿼리 스트링은 '()'를 이용해서 파라미터의 이름과 값을 지정.

<a th:href="@{/hello(name="AAA", age=16)}">Go to /hello </a>


 

📍  GET 방식으로 처리되는 링크에서 한글이나 공백 문자는 항상 주의해야 하는데 Thymeleaf를 이용하면 이에 대한 URL 인코딩 처리가 자동으로 이루어짐

<a th:href="@{/hello(name="한글처리", age=16)}">Go to /hello</a>


 

📍  만일 링크를 만드는 값이 배열과 같이 여러 개일 때는 자동으로 같은 이름의 파라미터를 처리

<a th:href="@{/hello(types=${{'AAA', 'BBB', 'CCC'}}, age=16}">Go to /hello</a>


5.  Thymeleaf의 특별한 기능들

1)  인라인 처리

Thymeleaf는 여러 편리한 점이 있지만 상황에 따라 동일한 데이터를 다르게 출력해 주는 인라인 기능은 자바 스크립트를 사용할 때 편리한 기능.

 

SampleController에 다양한 종류의 데이터를 Model에 담아서 전달하는 메서드를 추가

 

  • 추가되는 코드는 내부 클래스인 SampleDTO와 ex2()
  • SampleDTO를 정의할 때는 반드시 getter들을 만들어 줌
class SampleDTO {
        private String p1, p2, p3;

        public String getP1() {
            return p1;
        }

        public String getP2() {
            return p2;
        }

        public String getP3() {
            return p3;
        }

    }

 

@GetMapping("ex/ex2")
    public void ex2(Model model) {

        log.info("ex/ex2..........");

        List<String> strList = IntStream.range(1,10)
                .mapToObj(i -> "Data"+i)
                .collect(Collectors.toList());

        model.addAttribute("list", strList);

        Map<String, String> map = new HashMap<>();
        map.put("A", "AAAA");
        map.put("B", "BBBB");

        model.addAttribute("map", map);

        SampleDTO sampleDTO = new SampleDTO();
        sampleDTO.p1 = "Value -- p1";
        sampleDTO.p2 = "Value -- p2";
        sampleDTO.p3 = "Value -- p3";

        model.addAttribute("dto", sampleDTO);

    }

 

화면 구성을 위해 ex2.html을 추가
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div th:text="${list}"></div>
    <div th:text="${map}"></div>
    <div th:text="${dto}"></div>

    <script th:inline="javascript">

        const list = [[${list}]]

        const map = [[${map}]]

        const dto = [[${dto}]]

        console.log(list)
        console.log(map)
        console.log(dto)

    </script>
</body>
</html>

 

📍 HTML 코드를 이용하거나 자바 스크립트 코드를 이용할 때 같은 객체를 사용. 다만 차이점은 <script th:inline="javascript">가 지정된 점

📍 프로젝트를 실행해서 만들어진 결과를 보면 HTML은 기존처럼 출력되고, <script> 부분은 자바 스크립트에 맞는 문법으로 만들어진 것을 확인

 


2)  Thymeleaf의 레이아웃 기능

Thymeleaf의 <th:block>을 이용하면 레이아웃을 만들고 특정한 페이지에서는 필요한 부분만을 작성하는 방식으로 개발이 가능

 

레이아웃 기능을 위해서 별도의 라이브러리가 필요하므로 build.gradle에 추가
// 레이아웃 기능
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect', version: '3.1.0'

 

templates에 layout 폴더를 생성하고 레이아웃을 위한 layout1.html을 작성
<!DOCTYPE html>
<html lang="en"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Layout page</title>
</head>
<body>

    <div>
        <h3>Sample Layout Header</h3>
    </div>

    <div layout:fragment="content">
        <p>Page content goes here</p>
    </div>

    <div>
        <h3>Sample Layout Footer</h3>
    </div>

    <th:block layout:fragment="script">

    </th:block>

</body>
</html>

 

📍 코드 위쪽에는 http://www.ultraq.net.nz/thymeleaf/layout을 이용해서 Thymeleaf의 Layout을 적용하기 위한 네임스페이스를 지정
📍 코드 중간에는 layout:fragment 속성을 이용해서 해당 영역은 나중에 다른 파일에서 해당 부분만을 개발할 수 있음
     ➡️  layout1.html에는 content와 script 부분을 fragment로 지정

 

SampleController에 레이아웃 예제를 위한 ex3()을 추가
  @GetMapping("/ex/ex3")
    public void ex3(Model model) {
        model.addAttribute("arr", new String[] {"AAA", "BBB", "CCC"});
    }

 

templates의 ex폴더에 ex3.html을 생성. 가장 중요한 부분은 <html>에서 사용된 레이아웃 관련 설정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout1.html}">

<div layout:fragment="content">

    <h1>ex3.html</h1>

</div>

 

fragment에 content 부분만 작성한 것을 확인

 

 

layout1.html에는 content와 script 영역을 따로 구성했으므로 이를 이용해서 자바스크립트를 처리하고 싶다면 별도의 영역을 지정하고 fragment를 지정
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout/layout1.html}">

<div layout:fragment="content">

    <h1>ex3.html</h1>

</div>

<script layout:fragment="script" th:inline="javascript">

    const arr = [[${arr}]]

</script>

 


1.  스프링 부트란 ?

🐰  스프링 부트는 엄밀하게 말하면 '스프링 프레임워크 개발 도구'. 즉, 엔터프라이즈급 에플리케이션을 개발 하기 위해서 필요한 기능을 제공하는 개발 도구
🐰  스프링의 중요한 특징자동 설정 Auto Configuration (라이브러리만으로 설정을 인식하려는 특성) 을 내세울 수 있음
     예를 들어 스프링 부트는 데이터베이스와 관련된 모듈을 추가하면 자동으로 데이터베이스 관련 설정을 찾아서 실행
      ✓  스프링만을 이용하는 경우와 비교해보면 추가한 모듈 설정이 전혀 필요하지 않고, 설정 자체도 단순
🐰  다른 특징으로는 '내장 톰캣'과 단독 실행 가능한 도구라는 점. 스프링 부트는 별도의 서버 설정 없이도 개발이 가능하고, 실행도 가능

 

 

1) 기존 개발과 차이점


✓ 설정과 관련해서는 직접 필요한 라이브러리를 기존 build.gradle 파일에 추가하는 설정이 상당히 단순하기도 하지만 자동으로 처리

✓ 특히 톰캣이 내장된 상태로 프로젝트가 생성되기 때문에 WAS의 추가 설정이 필요하지 않다는 점도 편리
✓ 빈 설정은 XML을 대신해서 자바 설정을 이용하는 것으로 약간의 변경이 있음

스프링 MVC에서는 JSP를 이용할 수 있지만 기본 설정이 아니라서 라이브러리를 추가해야 되고, 스프링 부트는 Thymeleaf라는 템플릿 엔진을 활용하는 경우가 많음
스프링 부트에서도 MyBatis를 이용할 수 있지만, JPA를 이용하면 객체지향으로 구성된 객체들을 데이터베이스에 반영할 수 있는데 이를 자동으로 처리할 수 있으므로 별도의 SQL의 개발 없이도 개발이 가능

 

스프링 부트의 프로젝트 생성 방식


  👾  스프링 부트를 위한 프로젝트의 생성 방법은 크게 2가지
      -  Spring Initializr를 이용한 자동 생성 ( 대부분 많이 사용. 프로젝트의 기본 템플릿 구조를 만들어 주기 때문 )

            ➡️  웹 사이트(https://start.spring.io/) 에서 프로젝트를 생성하거나, 이클립스나 인텔리제이, VS Code 등에서도 Spring Initializr를 지원하기 때문에 호환성 면에서도 유리
      -  Maven이나 Gradle을 이용한 직접 생성

  👾  스프링 부트는 스프링을 쉽게 사용하기 위한 도구이므로 프로젝트를 생성하고 필요한 라이브러리들을 추가하는 형태의 개발도 가능


 

2.  프로젝트의 실행

🐰  스프링 부트의 프로젝트는 이미 서버를 내장한 상태에서 만들어지기 때문에 스프링만을 이용할 때와 달리 별 의 WAS (Web Application Server) 설정이 필요하지 않고 main() 메서드의 실행을 통해서 프로젝트를 실행

 

main()을 실행하면 자동으로 내장된 톰캣이 실행되는 것을 로그를 통해서 확인

  ✓  실행 결과는 에러가 발생
  ✓  스프링 부트가 자동 설정을 통해서 인식한 Spring Data JPA를 실행했을 때 DB와 관련된 설정을 찾을 수 없어서 발생한 에러

  📍  에러가 발생하긴 했지만, 아무런 설정이 없는 상태인데 자동으로 데이터베이스 관련 설정을 이용을 함
        ➡️  이와 같이 라이브러리만으로 설정을 인식하려는 특성을 '자동 설정 auto configuration'이라고 함

  📍  스프링 부트 설정은 프로젝트 생성 시에 만들어진 application.properties 파일을 이용하거나 application.yml (YAML 이라고 함) 파일을 이용할 수 있음
        ➡️  만일 파일 설정을 피하고 싶으면 @Configuration이 있는 클래스 파일을 만들어서 필요한 설정을 추가할 수 있음

  📍  대부분의 스프링을 지원하는 개발 도구 IDE에서는 application.properties 파일에 들어갈수 있는 내용을 쉽게 완성해 주는 기능을 제공

 

application.properties 파일에 데이터베이스 설정을 추가
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/sample
spring.datasource.username="아이디"
spring.datasource.password="비밀번호"

 

  💡  만일8080 port가 다른 프로젝트에서 실행되고 있다면 포트번호를 application.properties에서 server.port를 지정해서 변경할 수 있음

server.port=8082

 

서블릿이나 스프링처럼 프로젝트를 실행하면 브라우저가 자동으로 실행되지 않음. 부트의 경우 서버를 내장하고 있어서 브라우저가 자동 실행이 안되어서, 직접 브라우저를 열어서 주소창에 주소를 입력해야 함


 

3.  편의성을 높이는 몇 가지 설정

DataSource를 이용하는 것만으로도 스프링 부트가 편리하다는 사실을 알수 있지만, 추가적인 설정 몇 가지만 더 한다면 개발 시간을 더욱 단축할 수 있음

자동 리로딩 설정

 

  • 웹 개발 시에 코드를 수정하고 다시 deploy를 하는 과정을 자동으로 설정. Edit Configuration 메뉴를 실행
  • Bulid and run 메뉴에 있는 Modify options를 선택
  • On 'Update' action / On frame deactivation 의 옵션값을 Update classes and resources 로 지정

 

 

Lombok을 테스트 환경에서도 사용하기


스프링 부트는 체크박스를 선택하는 것만으로 Lombok 라이브러리를 추가하지만 테스트 환경에서는 설정이 빠져 있음
build.gradle 파일 내 dependencies 항목에 test 관련 설정을 조정

// lombok을 테스트 환경에서도 사용.
testCompileOnly('org.projectlombok:lombok')
testAnnotationProcessor('org.projectlombok:lombok')



로그 레벨의 설정


스프링 부트는 기본적으로 Log4j2가 추가되어 있기 때문에 라이브러리를 추가하지 않아도 됨.
applocation.properties 파일을 이용해서 간단하게 로그 설정을 추가할 수 있음.

logging.level.org.springframework=info
logging.level.kr.nomadlab=debug

 

 

인텔리제이의 DataSource 설정


인텔리제이 ultimate의 경우 JPA 관련 플러그인이 이미 설치되어 있기 때문에 DataSource를 설정해두면 나중에 엔티티 클래스의 생성이나 기타 클래스의 생성과 설정 시에 도움이 됨

 

 

테스트 환경과 의존성 주입 테스트


스프링에는 'spring-test-xxx' 라이브러리를 추가해야 하고 JUnit 등도 직접 추가 해야만 하지만, 스프링 부트는 프로젝트 생성할 때 이미 테스트 관련 설정이 완료되고 테스트 코드가 하나 생성되어 있음

 

  ✓  테스트 코드의 실행을 점검하기 위해서 DataSourceTest를 작성해서 HikariCP의 테스트와 Lombok을 확인

  ✓  DataSource는 application.properties에 설정된 DataSource 관련 설정을 통해서 생성된 빈 Bean이고, 이에 대한 별도의 설정 없이 스프링에서 바로 사용이 가능

package com.example.springboot;

import lombok.Cleanup;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
@Log4j2
public class DataSourceTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void connection() throws SQLException {
        @Cleanup Connection connection = dataSource.getConnection();

        log.info(connection);
        Assertions.assertNotNull(connection);
    }
}

 

 

Spring Data JPA을 위한 설정


DataSource 설정까지 모든 테스트가 완료되었다면 Spring Data JPA를 이용할 때 필요한 설정을 추가
application.properties에 다음과 같은 내용을 추가

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

 

  ✓  spring.jpa.hibernate.ddl-auto 속성은 프로젝트 실행 시 DDL 문을 처리 할 것인지를 명시
  ✓  DDL : Data Definition Language  ➡️  테이블이나 관계의 구조를 생성하는데 사용하며 CREATE, ALTER, DROP, TRUNCATE 문 등이 있음

  ✓  spring.jpa.properties.hibernate.format_sql 속성은 실제로 실행되는 SQL을 포맷팅해서 알아보기 쉽게 출력
  ✓  spring.jpa.show-sql은 JPA가 실행하는 SQL을 같이 출력

 

속성값 의미
none DDL을 하지 않음
create-drop 실행할 때 DDL을 실행하고 종료시에 만들어진 테이블 등을 모두 삭제
create 실행할 때마다 새롭게 테이블을 생성
update 기존과 다르게 변경돤 부분이 있을 때는 새로 생성
validate 변경된 부분만 알려주고 종료

 

📍  update 속성 값의 경우 테이블이 없을 때는 자동으로 생성하고 변경이 필요할 때는 alter table이 실행. 테이블뿐만 아니라 인덱스나 외래키 등도 자동으로 처리

 


 

4.  스프링 부트에서 웹 개발

🐰  스프링 부트를 이용해서 웹을 개발하는 일은 컨트롤러나 화면을 개발하는 것은 유사하지만, web.xml이나 server-context.xml과 같은 웹 관련 설정 파일들이 없기 때문에 이를 대신하는 클래스를 작성해 준다는 점이 다름
   ✓  xml을 통한 설정은 오류가 발생했을 때 찾기 힘듬. 마크업 언어라서 문법을 체크하는데 한계가 있음. 인텔리제이같은 통합 개발 환경에서도 자동완성을 사용하기 힘듬

 

1) 컨트롤러와 Thymeleaf 만들기

프로젝트에 우선 controller라는 패키지를 생성하고 SampleController 클래스를 생성
SampleController 자체의 개발은 기존의 스프링 MVC를 그대로 이용

@Controller
@Log4j2
public class SampleController {

    @GetMapping("/hello")
    public void hello(Model model) {

        log.info("hello...");
        model.addAttribute("msg", "Hello World");

    }
}

 

  📍 화면은 Thymeleaf를 이용하는데 위치를 주의해서 작성. 프로젝트 생성 시에 만들어져 있는 resources/templates 폴더에 hello.html을 작성
  📍 중요한 부분은 Thymeleaf의 네임스페이스 namespace를 추가

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${msg}"></h1>
</body>
</html>

 

 

 

Thymeleaf는 JSP와 동일하게 서버에서 결과물을 생성해서 보내는 방식이지만
좀더 HTML에 가깝게 작성할 수 있고 다양한 기능을 가지고 있음

 

 

 

 


 

2)  JSON 데이터 만들기

스프링을 사용할 때는 jackon-databind 라는 별도의 라이브러리를 출력한 후에 개발할 수 있지만, 스프링 부트는 'web' 항목을 추가할 때 자동으로 포함되므로 별도의 설정 없이 바로 개발할 수 있음

 

controller 패키지에 SampleJSONController라는 클래스를 작성
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
public class SampleJSONController {

    @GetMapping("/helloArr")
    public String[] helloArr() {
        log.info("HelloArr...");

        return new String[] {"AAA", "BBB", "CCC"};
    }

}

 

브라우저에 'helloArr' 경로를 호출하면 배열이 그대로 출력. 중요한 점은 서버에서 해당 데이터는 Content_Type을 'application/json' 방식 으로 전송

 


1.  대화상자

대화상자 dialog는 화면에 메시지를 나타낸 후 확인이나 취소 같은 사용자의 선택을 받아들이지는 경우에 사용
토스트보다 좀 더 강력한 메시지를 전할 때 적당


1)   기본 대화상자

대화상자는 사용자에게 중요한 사항을 알려준 후 사용자가 어떤 선택을 하게 하는 것이 주요 목적
그래서 사용자에게 좀 더 확실히 주지시켜야 할 때 혹은 계속 진행할지 여부를 선택하게 할 때 사용

대화상자의 가장 일반적인 사용 형식의 순서


   A.  대화상자 생성
      -  AlertDialog.Builer 클래스로 생성
 

   B.  용도에 따른 설정
      -  setTitle()  :  제목 설정
      -  setMessage()  :  내용 입력
      -  setIcon()  :  아이콘 설정
      -  setPositiveButton()  :  OK 버튼
      -  setNegativeButton()  :  Cancel 버튼
      -  setItems()  :  목록 출력

      - setSingleChoiceItems :  라디오 목록 출력
      - setMultiChoiceItems  :  체크박스 목록 출력

   C.  대화상자 화면 출력
      -  show()

<?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"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center_horizontal">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="대화상자"/>

</LinearLayout>
class MainActivity : AppCompatActivity() {

    lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button = findViewById(R.id.button)

        button.setOnClickListener {

            val dlg = AlertDialog.Builder(this@MainActivity)
            dlg.setTitle("제목입니다.")
            dlg.setMessage("이곳이 내용입니다.")
            dlg.setIcon(R.mipmap.ic_launcher)
            dlg.setPositiveButton("확인", null)
            dlg.show()
        }
    }
}

 

dlg.setPositiveButton("확인", null)
원형은 dlg.setPositiveButton("문자열", 리스너);


  확인이나 닫기 1개만 사용할 때는 setPositiveButton() 메서드를 사용
  리스너 부분이 null이라 대화상자만 닫힐 뿐 아무 동작이 일어나지 않음

 


 

2)  목록 대화상자

대화상자에 리스트 형태의 목록을 출력하고 그중 하나를 선택할 수 있음.가장 기본적인 목록을 만드는 형식

class MainActivity : AppCompatActivity() {

    lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button = findViewById(R.id.button)

        button.setOnClickListener {
            val versionArray = arrayOf("오레오", "파이", "큐(10)")
            val checkArray = booleanArrayOf(true, false, false)
            val dlg = AlertDialog.Builder(this@MainActivity)
            dlg.setTitle("좋아하는 버전은? ")
            dlg.setIcon(R.mipmap.ic_launcher)
            dlg.setMultiChoiceItems(versionArray, checkArray) {dialog, which, isCheked ->
                button.text = versionArray[which]
            }
            dlg.setPositiveButton("닫기", null)
            dlg.show()
        }
    }
}

 

val versionArray = arrayOf("오레오", "파이", "큐(10)")

 

    ✓  출력할 항목의 문자열 배열 생성

 

dlg.setItems(versionArray) {dialog, which ->
    button.text = versionArray[which]
}


    ✓  문자열에 클릭 이벤트 할당. which는 몇번째 항목인지 알려줌.
    ✓  선택과 동시에 창이 닫힘
    ✓ 선택해도 대화상자가 닫히지 않도록 하려면 setItems 대신 setSingleChoiceItems를 사용하면 되는데, 라디오버튼 같은 형태로 출력.  이 메서드는 setSingleChoiceItems(문자열 배열, 초기 선택 인덱스, 리스너)로 파라미터가 3개.

    ✓  여러 개를 동시에 선택하게 하려면 setMultiChoiceItems() 사용하며, 이는 체크박스 형태로 표시

 

val checkArray = booleanArrayOf(true, false, false)


    ✓  각 항목이 체크되었는지 boolean 배열로 만듦

 

dlg.setMultiChoiceItems(versionArray, checkArray) { dialog, which, isChecked ->


    ✓  setMultiChoiceItems()의 두 번째 파라미터는 첫 번째 파라미터인 문자열 배열과 개수가 같은 boolean 배열이어야 함

 

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts