[Android/Kotlin] 안드로이드 스튜디오에서 JDBC를 이용해 MySQL 연동하기 (3/3) - 통신편

코코아의 개발일지·2024년 1월 18일
1

Android-Kotlin

목록 보기
22/31
post-thumbnail

✍🏻 서론

학교 수업에서 요구한 기능은 다음과 같았다.

[필수 기능]
1. 회원가입
2. 팔로잉/팔로워 기능
3. 비밀번호 변경

그래서 우리 팀에서 세운 기능 목록은

기능쿼리문 종류
회원가입insert
로그인select
팔로워, 팔로잉 목록 조회select
팔로잉 및 취소insert & delete
비밀번호 변경update

이 정도였다.

사용한 쿼리문 종류를 보면 insert, select, delete, update가 있다.
그럼 각각의 경우에 코드를 어떻게 작성했는지를 살펴보겠다.

프로젝트 전체 구조에 대한 설명은 앞선 편들에서 확인할 수 있으니, 오늘은 다양한 쿼리문의 코드를 간단히 나열해보겠다.

[이전 시리즈]
1️⃣ 안드로이드 스튜디오에서 JDBC를 이용해 MySQL 연동하기 (1/3) - 세팅편
2️⃣ 안드로이드 스튜디오에서 JDBC를 이용해 MySQL 연동하기 (2/3) - 폴더 구조화편



💻 코드 작성

아래 코드는 DB 연결을 위해 기본적으로 사용되는 코드여서 코드 설명에서 생략하겠다.

private fun connectToDatabase(): Connection? {
	val url = resources.getString(R.string.db_url)
	val user = resources.getString(R.string.db_user)
	val password = resources.getString(R.string.db_password)

	return try {
		DriverManager.getConnection(url, user, password)
	} catch (e: SQLException) {
		null
	}
}

그럼 바로 insert부터 시작해 보자.


1️⃣ Insert

< 회원가입 >

[완성 화면]

회원가입_이름 회원가입_비밀번호 회원가입_완료

[쿼리문]

// 회원가입
<string name="query_insert_signup_user">
	INSERT INTO user(user_name, password)
  	VALUES (\'%1$s\', \'%2$s\')
</string>

유저가 다음 버튼을 누를 때 화면을 전환하면서 사용자 이름과 비밀번호를 넘기고, 마지막 회원가입 완료 화면에서 그 정보를 가지고 DB 연동을 진행한다.

[DB 연동 코드]

    private fun insertDatabaseData() {
        GlobalScope.launch(Dispatchers.IO) {

            val connection = connectToDatabase()

            if (connection != null) {
                connection.use { connection ->
                    // database에 회원가입 data insert
                    insertUserData(connection)
                }
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }


    private fun insertUserData(connection: Connection) {

        val sql = getString(R.string.query_insert_signup_user, userName, password);

        try {
            val statement = connection.createStatement()
            val resultSet = statement.executeUpdate(sql)

            Log.d("SignupFinishActivity", "회원가입 성공")
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: $sql")
            println(e.message)

            return
        }
    }

[추가 기능 생각해보기]

  • 닉네임 중복 체크나 비밀번호 유효성 검사 코드
  • DB에서 insert가 성공했을 때만 토스트 메시지를 띄우고, 화면을 이동하는 식으로 처리해줬으면 더 좋았을 거 같다.
  • 화면 디자인이 조금 바뀌었던데.. 이것도 디자인에 맞춰서 바꿔주면 더 좋을듯

< 팔로잉하기 >

[완성 화면]

팔로잉 팔로잉_취소

[쿼리문]

// 팔로잉 (팔로우하기)
    <string name="query_insert_following_my">INSERT INTO following VALUES (%1$d, %2$d);</string>
    <string name="query_insert_following_other">INSERT INTO follower VALUES (%2$d, %1$d);</string>

내가 팔로잉하면 그 사람의 팔로워에도 내가 추가되어야하기 때문에, 쿼리문을 두번 작성해 주었다.

[DB 연동 코드]

private fun onClickFollowDeleteBtn(follow: Follow, isFollowing: Boolean, isCancle: Boolean) {
        GlobalScope.launch(Dispatchers.IO) {
            val connection = connectToDatabase()
            if (connection != null) {
                if (isFollowing) { // 팔로워 삭제
                    ...
                } else { // 팔로잉
                    if (isCancle) { // 취소의 경우
                        ...
                    } else { // 팔로우를 다시 하는 경우 (재팔로우)
                        setFollowing(connection, follow.userId) { success ->
                            if (success) {
                                Log.d("FollowListFrag", "재팔로우 성공, user_name: ${follow.userName}")
                            }
                            // 연결 닫기
                            connection.close()
                        }
                    }
                }
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }
    
private fun setFollowing(connection: Connection, followingId: Int, onSuccess: (Boolean) -> Unit) {
        // 내가 팔로우
        val sql_my = String.format(resources.getString(R.string.query_insert_following_my), userId, followingId)
        // 그 사람의 팔로워에 나를 추가
        val sql_other = String.format(resources.getString(R.string.query_insert_following_other), userId, followingId)

        try {
            val statement = connection.createStatement()

            // 첫 번째 DELETE 쿼리 실행
            val rowsAffectedMy = statement.executeUpdate(sql_my)
            // 두 번째 DELETE 쿼리 실행
            val rowsAffectedOther = statement.executeUpdate(sql_other)

            // 양쪽 모두에서 영향을 받은 행(row)이 존재하면 성공으로 간주
            onSuccess(rowsAffectedMy > 0 && rowsAffectedOther > 0)
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: \n$sql_my\n$sql_other")
            println(e.message)
            onSuccess(false)
        }
    }

인스타그램에서 팔로워(나를 팔로우하는 사람)의 경우 삭제 시 바로 삭제되기 때문에 insert문을 쓸 필요가 따로 없다고 판단해서 '팔로잉만 팔로우 취소 후 재팔로우'를 구현했다.

[추가 기능 생각해보기]
실제 인스타에서는 공개 계정이랑 비공계 계정이 나눠져 있어서, 팔로잉 취소 후 재팔로우를 했을 경우

공계 계정 -> 회색 팔로잉 버튼
비공계 계정 -> 회색 요청됨 버튼

으로 나타나는데, 이 부분도 계정의 공개 여부에 따른 처리를 해줘도 좋을 거 같다.


2️⃣ Select

< 로그인 >

[완성 화면]

로그인_입력전 로그인_입력후 로그인 정보로 내 정보 불러오기

[쿼리문]

// 로그인으로 유저 정보 조회하기 (사용자 이름, 비밀번호 입력)
    <string name="query_select_login_user">SELECT u.user_id, u.user_name, u.name, u.profileImage_url,
    (SELECT count(*) FROM following WHERE user_id = u.user_id) AS following_count,
    (SELECT count(*) FROM follower WHERE user_id = u.user_id) AS follower_count
        FROM user u
        WHERE u.user_name = \'%1$s\' AND u.password = \'%2$s\';</string>

[DB 연동 코드]

private fun getDatabaseData() {
        GlobalScope.launch(Dispatchers.IO) {
            val connection = connectToDatabase()

            if (connection != null) {
                connection.use { connection ->
                    val user = getLoginUserInfo(connection)
                    Log.d("LoginActivity", user.toString())
                    // 유저 정보를 spf에 넣어줌
                    if (user != null) {
                        // 메인 액티비티로 이동
                        moveToMainActivity(user)
                    }
                }
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }

    fun getLoginUserInfo(connection: Connection): LoginUser? {
        val userName = binding.loginUserEt.text.toString()
        val pwd = binding.loginPwdEt.text.toString()

        // 쿼리 작성
        val sql = String.format(resources.getString(R.string.query_select_login_user), userName, pwd)

        return try {
            // Statement 객체를 생성하여 SQL 쿼리를 실행하기 위한 준비를 함
            val statement = connection.createStatement()
            // SQL SELECT 쿼리를 실행하고, 조회 결과를 테이블 형식의 데이터인 ResultSet 객체에 저장함
            val resultSet = statement.executeQuery(sql)

            var user = LoginUser(0, "", "", 0, 0, "")
            while (resultSet.next()) { // 조회 결과를 한줄 한줄 받아옴
                val id = resultSet.getInt("user_id")
                val userName = resultSet.getString("user_name")
                val name = resultSet.getString("name")
                val followerNum = resultSet.getInt("follower_count")
                val followingNum = resultSet.getInt("following_count")
                val profileImage = resultSet.getString("profileImage_url")
                user = LoginUser(id, userName, name, followerNum, followingNum, profileImage)
            }

            user
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: $sql")
            println(e.message)

            return null
        }
    }

[추가 기능 생각해보기]

  • 비밀번호를 틀렸을 때의 처리
  • 현재는 사용자 이름으로만 입력하게 해놨는데, EditText의 hint에 있는 내용처럼 이메일이나 전화번호를 입력하고도 회원가입이 가능하게 하면 좋을 거 같다.


< 팔로워, 팔로잉 목록 조회 >

이전 글에서 다룬 내용이긴 하지만 한 번 더 간단히 적어보겠다.

[완성 화면]

팔로워 탭 팔로잉 탭

[쿼리문]

// 팔로워, 팔로잉 목록 조회
<string name="query_select_follow_list">SELECT user_id, user_name, name, profileImage_url FROM user WHERE user_id IN (SELECT %1$s FROM %2$s WHERE user_id = %3$d)</string>

[DB 연동 코드]

private fun getDatabaseData() {
        GlobalScope.launch(Dispatchers.IO) {
            val connection = connectToDatabase()
            if (connection != null) {
                // DB에서 조회한 팔로워 or 팔로잉 정보를 리스트로 가져오기
                val users = getAllFollowList(connection)

                // UI 업데이트를 메인 스레드에서 수행
                withContext(Dispatchers.Main) {
                    follows.clear()
                    follows.addAll(users)
                    // 팔로워 or 팔로잉 리스트로 리사이클러뷰 데이터 초기화
                    initFollowRv()
                    // 연결 닫기
                    connection.close()
                }
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }

    fun getAllFollowList(connection: Connection): List<Follow> {
        // 현재 탭이 어떤 탭인지에 따라 서브쿼리 안에 적어줄 변수를 구분해주기 (팔로워를 조회할지 팔로잉을 조회할지)
        val targetId = if (isFollower) "follower_id" else "following_id" // 조회 대상이 되는 유저의 id
        val table = if (isFollower) "follower" else "following" // 조회할 테이블
        val sql = String.format(resources.getString(R.string.query_select_follow_list), targetId, table, userId)

        return try {
            // Statement 객체를 생성하여 SQL 쿼리를 실행하기 위한 준비를 함
            val statement = connection.createStatement()
            // SQL SELECT 쿼리를 실행하고, 조회 결과를 테이블 형식의 데이터인 ResultSet 객체에 저장함
            val resultSet = statement.executeQuery(sql)

            // 조회 결과를 반환할 리스트 (팔로워 또는 팔로잉 목록)
            val follows = ArrayList<Follow>()

            while (resultSet.next()) { // 조회 결과를 한줄 한줄 받아옴
                val id = resultSet.getInt("user_id")
                val userName = resultSet.getString("user_name")
                val name = resultSet.getString("name")
                val profileImage = resultSet.getString("profileImage_url")
                // 한 유저(팔로워, 팔로잉) 인스턴스에 받아온 필드를 차례대로 넣어줌
                val user = Follow(id, userName, name, profileImage)
                // 리스트에 위에서 받아온 유저 정보를 추가해줌
                follows.add(user)
            }
            // DB에서 조회한 팔로워, 팔로잉 리스트를 반환
            follows
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: $sql")
            println(e.message)

            ArrayList()
        }
    }

[추가 기능 생각해보기]

  • 검색 기능을 구현해봐도 좋을 것 같음


3️⃣ Delete

< 팔로워 삭제 >

[완성 화면]

팔로워 팔로워_삭제

[쿼리문]

// 팔로워 삭제
<string name="query_delete_follower_my">DELETE FROM follower WHERE user_id = %1$d AND follower_id = %2$d;</string>
<string name="query_delete_follower_other">DELETE FROM following WHERE user_id = %2$d AND following_id = %1$d;</string>

얘도 마찬가지로 팔로워 테이블과 팔로잉 테이블이 분리되어있기 때문에,
내가 팔로워를 삭제했다면 -> 그 사람의 팔로잉 목록에서 나를 삭제해줘야 해서 쿼리문이 두 개가 필요하다.
내가 팔로잉하는 사람과 나를 팔로잉하는 사람은 별개의 관계이기 때문에 테이블이 나뉘어야 한다.

[DB 연동 코드]

private fun onClickFollowDeleteBtn(follow: Follow, isFollowing: Boolean, isCancle: Boolean) {
        GlobalScope.launch(Dispatchers.IO) {
            val connection = connectToDatabase()
            if (connection != null) {
                if (isFollowing) { // 팔로워 삭제
                    deleteFollower(connection, follow.userId) { success ->
                        if (success) {
                            //TODO: 삭제 성공 시 토스트 메시지 표시

                            // 일단 로그 출력
                            Log.d("FollowListFrag", "팔로워 삭제 성공, user_name: ${follow.userName}")
                        }
                        // 연결 닫기
                        connection.close()
                    }
                } else { // 팔로잉
                    ...
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }

    private fun deleteFollower(connection: Connection, followerId: Int, onSuccess: (Boolean) -> Unit) {
        // 나를 팔로워하는 사람 삭제 (나의 팔로워)
        val sql_my = String.format(resources.getString(R.string.query_delete_follower_my), userId, followerId)
        // 그 사람의 팔로잉에서 나를 삭제
        val sql_other = String.format(resources.getString(R.string.query_delete_follower_other), userId, followerId)

        try {
            val statement = connection.createStatement()

            // 첫 번째 DELETE 쿼리 실행
            val rowsAffectedMy = statement.executeUpdate(sql_my)
            // 두 번째 DELETE 쿼리 실행
            val rowsAffectedOther = statement.executeUpdate(sql_other)

            // 양쪽 모두에서 영향을 받은 행(row)이 존재하면 성공으로 간주
            onSuccess(rowsAffectedMy > 0 && rowsAffectedOther > 0)
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: \n$sql_my\n$sql_other")
            println(e.message)
            // 삭제 실패
            onSuccess(false)
        }
    }

[추가 기능 생각해보기]

  • 현재는 팔로워를 삭제했을 때 줄어든 인원이 바로 반영이 안 되는데, 이 부분을 보완하고 싶음
  • 삭제 버튼 클릭 후 바로 삭제되는 것이 아닌, 실제 인스타처럼 삭제 전에 알림창을 띄워주고 싶음


< 팔로잉 취소 >

[완성 화면]

팔로잉 팔로잉_취소

[쿼리문]

// 팔로잉 취소
<string name="query_delete_following_my">DELETE FROM following WHERE user_id = %1$d AND following_id = %2$d;</string>
<string name="query_delete_following_other">DELETE FROM following WHERE user_id = %2$d AND following_id = %1$d;</string>

[DB 연동 코드]

private fun onClickFollowDeleteBtn(follow: Follow, isFollowing: Boolean, isCancle: Boolean) {
        GlobalScope.launch(Dispatchers.IO) {
            val connection = connectToDatabase()
            if (connection != null) {
                if (isFollowing) { // 팔로워 삭제
                    ...
                } else { // 팔로잉
                    if (isCancle) { // 취소의 경우
                        cancelFollowing(connection, follow.userId) { success ->
                            if (success) {
                                Log.d("FollowListFrag", "팔로잉 취소 성공, user_name: ${follow.userName}")
                            }
                            // 연결 닫기
                            connection.close()
                        }
                    } else { // 팔로우를 다시 하는 경우 (재팔로우)
                        ...
                    }
                }
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }

    private fun cancelFollowing(connection: Connection, followingId: Int, onSuccess: (Boolean) -> Unit) {
        // 내가 팔로잉을 취소
        val sql_my = String.format(resources.getString(R.string.query_delete_following_my), userId, followingId)
        // 그 사람의 팔로워에서 나를 삭제
        val sql_other = String.format(resources.getString(R.string.query_delete_following_other), userId, followingId)

        try {
            val statement = connection.createStatement()

            // 첫 번째 DELETE 쿼리 실행
            val rowsAffectedMy = statement.executeUpdate(sql_my)
            // 두 번째 DELETE 쿼리 실행
            val rowsAffectedOther = statement.executeUpdate(sql_other)

            // 양쪽 모두에서 영향을 받은 행(row)이 존재하면 성공으로 간주
            onSuccess(rowsAffectedMy > 0 && rowsAffectedOther > 0)
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: \n$sql_my\n$sql_other")
            println(e.message)
            onSuccess(false)
        }
    }

[추가 기능 생각해보기]

  • 얘도 팔로워 삭제처럼, 현재는 팔로우를 취소했을 때 줄어든 인원이 바로 반영이 안 되는데, 이 부부을 보완하고 싶음


4️⃣ Update

< 비밀번호 변경 >

[완성 화면]

비밀번호 비밀번호_입력

[쿼리문]

// 비밀번호 변경
<string name="query_update_password">
      UPDATE user SET password = \'%1$s\' 
      WHERE password = \'%2$s\' AND user_id = %3$d;
</string>

[DB 연동 코드]

private fun updateDatabaseData() {
        GlobalScope.launch(Dispatchers.IO) {

            val connection = connectToDatabase()

            if (connection != null) {
                connection.use { connection ->
                    // 비밀번호 업데이트
                    updateUserPassword(connection)
                }
            } else {
                Log.d("Database", "Failed to connect to the database.")
            }
        }
    }

    private fun updateUserPassword(connection: Connection) {
        val currentPwd = binding.editPasswordCurrentPwdEt.text
        val newPwd = binding.editPasswordNewPwdEt.text

        val sql = getString(R.string.query_update_password, newPwd, currentPwd, getUserId())

        try {
            val statement = connection.createStatement()
            val resultSet = statement.executeUpdate(sql)

            //TODO: 비밀번호 업데이트가 제대로 되지 않았다면 (기존 비밀번호와 다르다면) 오류 메시지 띄워주기

            Log.d("EditPasswordActivity", "비밀번호 업데이트 성공")
            // 종료
            finish()
        } catch (e: SQLException) {
            println("An error occurred while executing the SQL query: $sql")
            println(e.message)

            return
        }
    }

[추가 기능 생각해보기]

  • 입력한 기존 비밀번호가 틀리다면 오류 메시지 띄워주기
  • 새 비밀번호와 재확인 번호가 같을 때만 버튼 활성화 해주기


🫠 마치며

사실 자바 스윙으로는 화면을 만들기는 싫다는('안드로이드를 배웠는데 내가 왜?'), 단순한 고집으로 선택한 방법이었는데 이 방법도 생각보다 힘들었다. 그래도 과정 자체는 스윙과 비슷하다고 생각되는데, 내게는 안드로이드 코딩이 훨씬 더 익숙하기에 제법 괜찮게 했다. Java vs Kotlin이라는 차이점도 있고..
팀플이라고 부르기 민망할 정도로 프론트를 내가 거의 다 했지만 새롭게 알게된 것도 많았고, 어떻게 하면 더 좋은 구조를 만들 수 있을까 생각도 해보고... 돌아보면 그래도 얻어간 건 많다(그 고생을 했는데 얻어가기라도 해야지). 팀플이란 이름 아래 화나는 일이 조금 있긴 했어도 코딩할 때는 나름 재밌었다. 블로그를 3편이나 뽑았다는 점도 뭐.. 좋게 생각하기로 했다ㅎㅎ 우리 조 발표 때 스윙으로 만들었던 다른 조에서 탄성이 나왔던 거나, 교수님이 "이 팀은 그냥 인스타를 만들었네~" 말씀하신 게 그동안 힘들게 만들었던 것의 보상을 그나마 받은 느낌...?

팀플 기간에 코드를 내가 죄다 짰던 기억 때문에 다른 사람들에게는 추천하고 싶지 않은 방법이지만,
만약에 나같은 모종의 이유로 이 방법을 택해야하는 사람이 있다면, 그런 사람들에게 조금이나마 도움이 되길 바라며 글을 써본다.

더 자세한 코드는 아래 깃허브에서 찾아볼 수 있다.

[JDBC를 이용해 Android와 MySQL를 연동한 인스타 클론코딩 Github]
🔗 https://github.com/nahy-512/Database_TeamD

기나긴 안드로이드에서 MySQL 연동하기!
이만 끝을 내보겠다.

profile
우당탕탕 성장하는 개발자

0개의 댓글