WebSocket이란
지난번 포스팅으로 대체하겠다.
https://superohinsung.tistory.com/352
Android Project에서 채팅 기능 구현기 + Clean Architecture + Hilt
졸업작품에서 채팅기능을 구현해야하는 일이 있었다.
실시간 채팅기능이었다.
아래 코드와 함께 풀이를 한번 적어보겠다.
프로젝트는 우선적으로 클린아키텍처 기반의 Hilt를 사용한 의존성주입을 사용하였다.
문제는 크게 2가지로
1. WebScoketListener에 대한 위치
2. 채팅 연결 및 해제 처리
아래는 코드를 통해서 내가 한 방식에 대해서 살펴보자.
Data 모듈 -> AppModule
@Module
@InstallIn(ViewModelComponent::class)
object AppModule {
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.pingInterval(30, TimeUnit.SECONDS)
.build()
}
@Provides
fun provideWebSocketDataSource(client: OkHttpClient): WebSocketDataSource {
return WebSocketDataSourceImpl(client)
}
@Provides
fun provideVideoRepository(webSocketDataSource: WebSocketDataSource): ChattingRepository {
return ChattingRepositoryImpl(webSocketDataSource)
}
}
우선 AppModule에서 Hilt를 사용하여 ViewModelComponant로 선언하였다. 이유는 ViewModel에서 주로 사용될 기능이라고 생각했고, 또한 ViewModel과 Activity Componant의 생명주기가 비슷했기에 굳이 Activity로 선언하지 않아도 된다고 생각했다.
또한 Okhttps는 WebSocket을 위해서 필요할때마다 사용하기위해 Singleton으로 선언하였으며, 아래는 메서드를 제공하기위한 Provide로 선언하였다.
Data모듈 -> source -> WebScoketDataSourceImpl & WebScoketDataSource
class WebSocketDataSourceImpl @Inject constructor(
private val client: OkHttpClient
) : WebSocketDataSource {
private var webSocket: WebSocket? = null
override fun connect(url: String, listener: WebSocketListener) {
val request = Request.Builder()
.url(url)
.build()
webSocket = client.newWebSocket(request, listener)
}
override fun disconnect() {
Log.d("disconnect", "disconnect")
webSocket?.cancel()
webSocket = null
}
override fun sendMessage(roomId: Int, senderId: String, message: String) {
val messageJson = JSONObject().apply {
put("roomId", roomId)
put("senderId", senderId)
put("message", message)
}
if (webSocket?.send(messageJson.toString()) == true) {
Log.d("message success", messageJson.toString())
} else {
Log.d("message false", messageJson.toString())
}
}
}
interface WebSocketDataSource {
fun connect(url: String, listener: WebSocketListener)
fun disconnect()
fun sendMessage(roomId: Int, senderId: String, message: String)
}
AppModule에서 선언한 WebScoketDataSource의 구체화 부분이다.
WebScoket에 대한 연결 및 해제와 메세지를 보냈을 때의 부분을 구체화하였다.
또한 Log를 통해서 지속적으로 메세지를 잘 주고 받았는지를 확인하였다.
Data모듈 -> repository -> ChattingRepositoryImpl
class ChattingRepositoryImpl @Inject constructor(
private val webSocketDataSource: WebSocketDataSource
) : ChattingRepository {
override fun connect(url: String, listener: WebSocketListener) =
webSocketDataSource.connect(url = url, listener = listener)
override fun disconnect() = webSocketDataSource.disconnect()
override fun sendMessage(roomId: Int, senderId: String, message: String) =
webSocketDataSource.sendMessage(roomId = roomId, senderId = senderId, message = message)
}
본격적으로 Domain에서의 ChattingRepository를 구체화하여 연결하기위한 레포지토리이다.
Domain모듈 -> repository -> ChattingRepository
interface ChattingRepository {
fun connect(url: String, listener: WebSocketListener)
fun disconnect()
fun sendMessage(roomId: Int, senderId: String, message: String)
}
이부분은 Presentation 모듈에서 의존성을 사용하기 위한 인터페이스이다.
Presentation모듈 -> chatting -> ChattingViewModel
@HiltViewModel
class ChattingViewModel @Inject constructor(
private val successMatchingUseCase: SuccessMatchingUseCase,
private val applyScoreUseCase: ApplyScoreUseCase,
private val getUserUseCase: GetUserUseCase,
private val chattingRepository: ChattingRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(ChattingUiState())
val uiState = _uiState.asStateFlow()
private var fetchJob: Job? = null
fun bind(postId: Int, roomId: Int, roomTitle: String) {
_uiState.update {
it.copy(
postId = postId,
roomTitle = roomTitle,
roomId = roomId,
senderId = getUserUseCase()!!.id
)
}
}
fun bindChatting(chatting: MutableList<ChatMessage>) {
_uiState.update {
it.copy(
chatting = chatting
)
}
}
fun connectToWebSocket(roomId: Int) {
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
Log.d("onOpen", response.code.toString())
}
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
// 서버에서 텍스트 메시지를 받았을 때 처리하는 로직, 예: 기본 메시지 또는 이전 채팅 메시지 받기
try {
_uiState.update {
it.copy(
isLoading = true
)
}
val jsonArray = JSONArray(text)
val chatMessages = ArrayList<ChatMessage>()
for (i in 0 until jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
chatMessages.add(
ChatMessage(
roomId = jsonObject.getInt("roomId"),
senderId = jsonObject.getString("senderId"),
message = jsonObject.getString("message"),
)
)
}
bindChatting(chatMessages)
_uiState.update {
it.copy(
isLoading = false
)
}
} catch (e: Exception) {
_uiState.update {
it.copy(
isLoading = true
)
}
val json = JSONObject(text)
val chatMessage = ChatMessage(
roomId = json.getInt("roomId"),
senderId = json.getString("senderId"),
message = json.getString("message")
)
Log.d("ChatMessage", chatMessage.toString())
Log.d("uiState.value.chatting", uiState.value.chatting.toString())
uiState.value.chatting.add(chatMessage)
Log.d("uiState.value.chatting", uiState.value.chatting.toString())
val chatting = uiState.value.chatting
_uiState.update {
it.copy(
chatting = chatting, isLoading = false
)
}
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
Log.d("onClosed", code.toString())
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
super.onMessage(webSocket, bytes)
Log.d("onMessage", bytes.toString())
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
Log.d("onFailure", response.toString())
}
// 오류 발생시 처리
}
chattingRepository.connect("링크${roomId}", listener)
}
fun disconnectFromWebSocket() {
chattingRepository.disconnect()
}
fun sendMessage() {
val senderId = uiState.value.senderId
val roomId = uiState.value.roomId!!
val message = uiState.value.myChatMessage
viewModelScope.launch {
chattingRepository.sendMessage(roomId = roomId, senderId = senderId, message = message)
}
}
// (생략)
WebScoketListener에 대한 구체적인 부분을 ViewModel에 구현하였는데, 가장 아쉬운 부분은 아마 이 부분일 것이다.
당시 시간압박에 쫒겨서 깊게 생각하기 보다는 빠르게 구현을 하는것에 가장 큰 목적을 두었다.
아마 이부분을 리팩토링할 수 만 있었더라면 WebSocketListener를 Data모듈에 두고 사용하지 않을까 싶다.
또한 프로젝트 시범에서는 많은 양의 채팅을 사용하지 않을 것이라는 생각이 있었기에 단순 ArrayList로 사용하였지만, 채팅의 양이 늘어난다면 메모리 초과 오류 또한 발생했을 것이다.
'Android > Study' 카테고리의 다른 글
[Android] SearchView 사용기 (0) | 2024.06.24 |
---|---|
[Android] WebView 사용기 (0) | 2024.06.24 |
[Android] android Github Actions CI/CD를 사용기 (0) | 2024.06.22 |
[Android] ConstraintLayout의 장점 (0) | 2024.06.11 |
[Android] RecyclerView LayoutManager에 대해서 공부하자 (0) | 2024.06.10 |