안드로이드 스터디

Retrofit과 백엔드와의 연동

mky 2025. 12. 30. 13:52

Retrofit 라이브러리를 사용해 서버와 api연동을 하는 법을 알아보자!!

 

1. API관련 설정

 1. Module 수준 build.gradle에 아래의 의존성을 추가한다,

// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
implementation("com.squareup.retrofit2:adapter-rxjava2:2.9.0")

// okHttp - 실제로 HTTP 통신을 수행하는 네트워크 엔진
implementation("com.squareup.okhttp3:okhttp:4.9.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")

// Glide
implementation("com.github.bumptech.glide:glide:4.12.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.12.0")

 

 2. Manifest에 가서 인터넷 설정을 허용한다.

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

 

 3. Postman을 켜고 아래의 API 명세서에 있는 내용을 확인한다. 또한, 직접 API의 엔드포인트에 더미데이터를 넣어 테스트해본다.

https://softsquared.notion.site/API-bd3013da9d384c3e820bef4eeaac111c

 

(유데미) 안드로이드 API 명세서 | Notion

https://edu-api-test.softsquared.com

softsquared.notion.site

 

2. API 연동

1. 먼저 User data class에 name 필드를 추가하고, Json 변환을 위해 @SerializedName 어노테이션을 추가한다.

package com.example.flo.data.user

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName

@Entity(tableName = "UserTable")
data class User(
    @SerializedName(value = "email") var email: String,
    @SerializedName(value = "password") var password: String,
    @SerializedName(value = "name") var name: String
){
    @PrimaryKey(autoGenerate = true) var id: Int = 0
}

 

 2. 서버의 응답을 받을 data class를 정의하기 위해 BaseResponse data class를 생성한다.

data class BaseResponse(
    val isSuccess : Boolean,
    val code : Int,
    val message : String
)

 

3. AuthApi라는 이름의 인터페이스를 생성한다. 이 인터페이스는 api를 정의한 서비스 로직? 이라고 보면 된다.

interface AuthApi {
    @POST("/users")
    fun signUp(@Body user : User) : Call<AuthResponse>
}

의미를 설명하자면, User 객체를 (JSON으로 변환해서) 요청 body에 담아 보낸다는 뜻이다.

 

4.  ApiRepository 클래스를 생성한다.

class ApiRepository {
    companion object {
        const val BASE_URL = "https://edu-api-test.softsquared.com"
    }
}

 

5. RetrofitInstance라는 이름의 클래스를 생성한다.

class RetrofitInstance {
    companion object {
        private val retrofit by lazy {
            Retrofit.Builder()
                .baseUrl(ApiRepository.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
        }
        val authApi = retrofit.create(AuthApi::class.java)
    }
}

싱글톤으로 만든다. by lazy는 처음 접근될 때 딱 한 번만 생성되고 이후엔 같은 객체를 재사용한다는 키워드이다. 쓰레드 안전 (기본 설정) 이라는 장점도 있다. Retrofit은 무거운 객체라서 이 패턴이 정석이다,

Retrofit객체를 만들고 그 Retrofit이 AuthApi 인터페이스 구현체를 자동 생성해주는 코드이다.

 

6. SignUpActivity를 아래와 같이 수정한다.

class SignUpActivity : AppCompatActivity() {

    lateinit var binding: ActivitySignUpBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySignUpBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.signUpSignUpBtn.setOnClickListener {
            signUp()
        }
    }

    //API 요청용 User 데이터 준비 함수
    private fun getUser() : User {
        val email : String = binding.signUpIdEt.text.toString() + "@" + binding.signUpDirectInputEt.text.toString()
        val pwd : String = binding.signUpPasswordEt.text.toString()
        var name : String = binding.signUpNameEt.text.toString()

        return User(email, pwd, name)
    }

    //회원가입 전체 로직 메서드. API호출 담당
    private fun signUp() : Boolean {
    
        //예외 처리
        if(binding.signUpIdEt.text.toString().isEmpty() || binding.signUpDirectInputEt.text.toString().isEmpty()) {
            Toast.makeText(this, "이메일 형식이 잘못되었습니다.", Toast.LENGTH_SHORT).show()
            return false
        }

        if(binding.signUpPasswordEt.text.toString() != binding.signUpPasswordCheckEt.text.toString()) {
            Toast.makeText(this, "비밀번호가 일치하지 않습니다.", Toast.LENGTH_SHORT).show()
            return false
        }

        if(binding.signUpNameEt.text.toString().isEmpty()) {
            Toast.makeText(this, "이름 형식이 잘못되었습니다.", Toast.LENGTH_SHORT).show()
            return false
        }

        //Retrofit 회원가입 요청
        RetrofitInstance.authApi.signUp(getUser()).enqueue(object: Callback<BaseResponse> {
            override fun onResponse(call: Call<BaseResponse>, response: Response<BaseResponse>) {
                Log.d("SignUp-Success", response.toString())
                val response : BaseResponse = response.body()!!
                when(response.code) {
                    1000 -> finish() // 성공
                    2016, 2018 -> {
                        binding.signUpEmailErrorTv.visibility = View.VISIBLE
                        binding.signUpEmailErrorTv.text = response.message
                    }
                }
                
            }

            override fun onFailure(call: Call<BaseResponse>, t: Throwable) {
                Log.d("SignUp-Failure", t.message.toString())
            }
        })
        Log.d("SignUpActivity", "All Finished")

        return true
    }
}

 

여기서 API를 호출하는 코드를 집중적으로 봐보자.

RetrofitInstance.authApi.signUp(getUser()).enqueue(object: Callback<BaseResponse> {
            override fun onResponse(call: Call<BaseResponse>, response: Response<BaseResponse>) {
                Log.d("SignUp-Success", response.toString())
                val response : BaseResponse = response.body()!!
                when(response.code) {
                    1000 -> finish() // 성공
                    2016, 2018 -> {
                        binding.signUpEmailErrorTv.visibility = View.VISIBLE
                        binding.signUpEmailErrorTv.text = response.message
                    }
                }
                
            }

            override fun onFailure(call: Call<BaseResponse>, t: Throwable) {
                Log.d("SignUp-Failure", t.message.toString())
            }
        })
        Log.d("SignUpActivity", "All Finished")

이 코드는 회원가입 API 요청을 “비동기”로 보내고, 응답이 오면 콜백(onResponse / onFailure)에서 처리한다.

저기서 code는 http코드가 아니고, 서버가 직접 정의한 규칙 코드이다.

Call은 "API 요청 하나를 대표하는 객체" 이고, Response<BaseResponse>는 " 서버가 실제로 돌려준 HTTP 응답 전체"이다.

 

이제 코드를 실행해보자. 지금은 서버가 켜져있지 않아 회원가입이 잘 처리되는 것까지는 확인할 수 없지만, 네트워크 연결이 없는 상태에서 회원가입을 시도해도, 프로세스가 종료되지 않고 로그를 찍는다면 성공한 것이다.

 

3. API모듈화

Activity에 여러가지 API를 정의하다보면, 코드의 직관성과 가독성이 크게 저해된다. 그러므로 이러한 일을 방지하려면, API를 모듈화하여 정의하는 것이 좋다. 그러면 지금부터 위에서 실습한 내용을 모듈화하는 방법에 대해 알아보자.

 

1. SignUpView 인터페이스를 생성한다.

interface SignUpView {
    fun onSignUpSuccess()
    fun onSignUpFailure(message : String)
}

SignUpActivity와 회원가입 API 간의 인터페이스이다.

 

2. SignUpActivity에서 SignUpView 인터페이스를 상속해야 한다.

class SignUpActivity : AppCompatActivity(), SignUpView {

 

3. AuthService 클래스를 생성한다.

class AuthService {
    private lateinit var signUpView: SignUpView

    fun setSignUpView(signUpView: SignUpView) {
        this.signUpView = signUpView
    }

    fun signUp(user : User) {
        RetrofitInstance.authApi.signUp(user).enqueue(object: Callback<BaseResponse> {
            override fun onResponse(call: Call<BaseResponse>, response: Response<BaseResponse>) {
                Log.d("SignUp-Success", response.toString())
                val response : BaseResponse = response.body()!!
                when(response.code) {
                    1000 -> signUpView.onSignUpSuccess()
                    else -> signUpView.onSignUpFailure(response.message)
                }
            }
            override fun onFailure(call: Call<BaseResponse>, t: Throwable) {
                Log.d("SignUp-Failure", t.message.toString())
            }
        })
        Log.d("SignUpActivity", "All Finished")
    }
}
  • setSignUpView 
    • 연결될 Activity를 설정한다.
    • 연결될 Activity에 onSignUpSuccess(), onSignUpFailure()가 정의되어 있어야 한다.
  • 서버의 응답 코드가 1000이면, 연결된 Activity(SignUpActivity)의 onSignUpSuccess() 메서드를 호출하고, 그 외의 응답 코드에 대해서는 onSignUpFailure() 메서드를 호출한다.

4. SignUpActivity를 아래와 같이 수정한다.

class SignUpActivity : AppCompatActivity(), SignUpView {
	...
    
    private fun signUp() : Boolean {
        if(binding.signUpIdEt.text.toString().isEmpty() || binding.signUpDirectInputEt.text.toString().isEmpty()) {
            Toast.makeText(this, "이메일 형식이 잘못되었습니다.", Toast.LENGTH_SHORT).show()
            return false
        }

        if(binding.signUpPasswordEt.text.toString() != binding.signUpPasswordCheckEt.text.toString()) {
            Toast.makeText(this, "비밀번호가 일치하지 않습니다.", Toast.LENGTH_SHORT).show()
            return false
        }

        if(binding.signUpNameEt.text.toString().isEmpty()) {
            Toast.makeText(this, "이름 형식이 잘못되었습니다.", Toast.LENGTH_SHORT).show()
            return false
        }

        val authService = AuthService()
        authService.setSignUpView(this) // 객체를 통한 멤버 함수 호출


        authService.signUp(getUser())
        return true
    }

    override fun onSignUpSuccess() {
        finish()
    }

    override fun onSignUpFailure(message : String) {
        binding.signUpEmailErrorTv.visibility = View.VISIBLE
        binding.signUpEmailErrorTv.text = message
    }
}

API 모듈화를 적용함으로써 SignUpActivity의 직관성 및 가독성이 크게 향상된 것을 확인할 수 있다.

 

4. API를 활용하여 로그인 구현하기

 

1. LoginView 인터페이스를 생성한다.

interface LoginView {
    fun onLoginSuccess(code : Int, result : Result)
    fun onLoginFailure(message : String)
}

LoginActivity와 로그인 API 간의 인터페이스이다.

 

2. LoginActivity에서 LoginView 인터페이스를 상속해야 한다.

class LoginActivity : AppCompatActivity(), LoginView {

 

3. AuthApi에 아래의 내용을 추가한다.

@POST("/users/login")
fun login(@Body user : User) : Call<BaseResponse>

 

4. 로그인 API에는 응답객체의 Body에 result 값이 포함되므로, BaseResponse를 아래와 같이 수정해야 한다.

data class BaseResponse(
    @SerializedName("isSuccess") val isSuccess : Boolean,
    @SerializedName("code") val code : Int,
    @SerializedName("message") val message : String,
    @SerializedName("result") val result : Result?
)

data class Result (
    @SerializedName("userIdx") var userIdx : Int,
    @SerializedName("jwt") var jwt : String
)

result에만 nullable처리를 해준 이유는 응답에 실패했을 시 result = null이 되기 때문이다.

 

5. AuthService에 아래의 내용을 입력한다.

class AuthService {
    private lateinit var signUpView: SignUpView
    private lateinit var loginView: LoginView

    fun setSignUpView(signUpView: SignUpView) {
        this.signUpView = signUpView
    }

    fun setLoginView(loginView: LoginView) {
        this.loginView = loginView
    }

    fun signUp(user : User) {
        RetrofitInstance.authApi.signUp(user).enqueue(object: Callback<BaseResponse> {
            override fun onResponse(call: Call<BaseResponse>, response: Response<BaseResponse>) {
                Log.d("SignUp-Success", response.toString())
                val response : BaseResponse = response.body()!!
                when(response.code) {
                    1000 -> signUpView.onSignUpSuccess()
                    else -> signUpView.onSignUpFailure(response.message)
                }
            }
            override fun onFailure(call: Call<BaseResponse>, t: Throwable) {
                Log.d("SignUp-Failure", t.message.toString())
            }
        })
        Log.d("SignUpActivity", "All Finished")
    }

    fun login(user : User) {
        RetrofitInstance.authApi.login(user).enqueue(object: Callback<BaseResponse> {
            override fun onResponse(call: Call<BaseResponse>, response: Response<BaseResponse>) {
                Log.d("Login-Success", response.toString())
                val response : BaseResponse = response.body()!!
                when(val code = response.code) {
                    1000 -> loginView.onLoginSuccess(code, response.result!!)
                    else -> loginView.onLoginFailure(response.message)
                }
            }
            override fun onFailure(call: Call<BaseResponse>, t: Throwable) {
                Log.d("Login-Failure", t.message.toString())
            }
        })
        Log.d("LoginActivity", "All Finished")
    }
}

 

6. LoginActivity에 아래의 내용을 추가한다.

class LoginActivity : AppCompatActivity(), LoginView {
	...
    
    private fun login() {
        if (binding.loginIdEt.text.toString().isEmpty() || binding.loginDirectInputEt.text.toString().isEmpty()) {
            Toast.makeText(this, "이메일을 입력해주세요", Toast.LENGTH_SHORT).show()
            return
        }

        if (binding.loginPasswordEt.text.toString().isEmpty()) {
            Toast.makeText(this, "비밀번호를 입력해주세요", Toast.LENGTH_SHORT).show()
            return
        }

        val email : String = binding.loginIdEt.text.toString() + "@" + binding.loginDirectInputEt.text.toString()
        val pwd : String = binding.loginPasswordEt.text.toString()

        val authService = AuthService()
        authService.setLoginView(this)

        authService.login(User(email, pwd, ""))
    }
    
    private fun saveJwtFromServer(jwt : String) {
        val spf = getSharedPreferences("auth2", MODE_PRIVATE)
        val editor = spf.edit()

        editor.putString("jwt", jwt)
        editor.apply()
	}

	override fun onLoginSuccess(code : Int, result : Result) {
        saveJwtFromServer(result.jwt)
        startMainActivity()
    }

    override fun onLoginFailure(message : String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
	...

 

7. MainActivity에 아래의 내용을 추가한다.

class MainActivity : AppCompatActivity() {
	...
    
    override fun onCreate(savedInstanceState: Bundle?) {
		...
    	Log.d("MainActivity", getJwt().toString())
    }
    
	private fun getJwt() : String? {
	    val spf = this.getSharedPreferences("auth2", MODE_PRIVATE)
	    
	    return spf!!.getString("jwt", "")
	}
    ...

sharedPreferences에 저장된 jwt 토큰을 앱 시작 시 불러와 로그인 상태를 확인하기 위한 코드이다.

이 다음 단계는 보통

  • jwt 있으면 → 메인
  • jwt 없으면 → 로그인 화면

으로 이어지게 된다.

이제 코드를 실행한 후 회원가입 한 계정으로 로그인을 시도해보자. MainActivity 태그의 로그를 확인해보면, jwt 토큰 정보가 콘솔에 출력되는 것을 확인할 수 있을 것이다.

 

'안드로이드 스터디' 카테고리의 다른 글

JetPack Compose - 1  (0) 2025.12.30
앱의 신분증 : Token과 인증  (0) 2025.12.30
RecyclerView CRUD 구현하기  (2) 2025.11.02
RecyclerView 클릭 이벤트 구현  (0) 2025.11.02
RecyclerView를 써 보자!  (0) 2025.11.02