학교 수업에서 요구한 기능은 다음과 같았다.
[필수 기능]
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부터 시작해 보자.
[완성 화면]
회원가입_이름 | 회원가입_비밀번호 | 회원가입_완료 |
---|
[쿼리문]
// 회원가입
<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
}
}
[추가 기능 생각해보기]
[완성 화면]
팔로잉 | 팔로잉_취소 |
---|
[쿼리문]
// 팔로잉 (팔로우하기)
<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문을 쓸 필요가 따로 없다고 판단해서 '팔로잉만 팔로우 취소 후 재팔로우'를 구현했다.
[추가 기능 생각해보기]
실제 인스타에서는 공개 계정이랑 비공계 계정이 나눠져 있어서, 팔로잉 취소 후 재팔로우를 했을 경우
공계 계정
-> 회색팔로잉
버튼
비공계 계정
-> 회색요청됨
버튼
으로 나타나는데, 이 부분도 계정의 공개 여부에 따른 처리를 해줘도 좋을 거 같다.
[완성 화면]
로그인_입력전 | 로그인_입력후 | 로그인 정보로 내 정보 불러오기 |
---|
[쿼리문]
// 로그인으로 유저 정보 조회하기 (사용자 이름, 비밀번호 입력)
<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
}
}
[추가 기능 생각해보기]
이전 글에서 다룬 내용이긴 하지만 한 번 더 간단히 적어보겠다.
[완성 화면]
팔로워 탭 | 팔로잉 탭 |
---|
[쿼리문]
// 팔로워, 팔로잉 목록 조회
<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()
}
}
[추가 기능 생각해보기]
[완성 화면]
팔로워 | 팔로워_삭제 |
---|
[쿼리문]
// 팔로워 삭제
<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)
}
}
[추가 기능 생각해보기]
[완성 화면]
비밀번호 | 비밀번호_입력 |
---|
[쿼리문]
// 비밀번호 변경
<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 연동하기!
이만 끝을 내보겠다.