👋 들어가기 전
우리 서비스 핵심 기능인 스트리밍 화면을 살펴보자.
1. 영상 송출
2. 방송 정보 제공
3. 실시간 채팅기능
우리는 스트리밍관련 해서 naver cloud platform의 live station을 제공 받는다.
여기서 1번은 라이브 스테이션에서 제공을 받으면 되지만
방송 정보 (제목, 부가 설명) 제공하는 api와 실시간으로 채팅할 서버가 없다는 것을 확인했다.
현실적인 한계를 깨닫고 라이브 스트리밍 서비스에서 채팅 기능을 빼야하나 ???
아니면 FireBase를 이용해서 손쉽게 적용할까??
하지만 위 2개는 부스트캠프의 정신인 도전과는 굉장히 멀어보인다.
그래서 나는 클라이언트 쪽 웹소켓과 서버를 직접 만들기로 결심했다.
클라이언트 쪽 웹소켓은 다른 포스팅에서 따로 설명하고 링크를 남기겠다.
❓ 스프링을 선택한 이유
스프링을 선택한 첫번째 이유는 프레임워크 언어가 그나마 익숙한 Kotlin이여서 선택한 것이 가장크다.
node.js도 사용해봤지만 너무 오래전 일이고 js의 불안전한 타입 인식이 좋지 않게 느껴졌다.
두번 째 이유는 약간의 홍대병 ??.. 이왕 서버 개발의 도전을 하니깐 해보지 않은 프레임워크 도전도
신선할 것 같다고 생각해서 결정을 했다.
결국 언어는 어느정도 알고 있고 프레임워크만 집중하면 되기 때문에 "스프링"으로 결정했다.
📦 의존성 설치
intellji ide 무료 버전을 쓰다보니 spring을 외부에서 만들어서 줘야한다고 한다.
굉장히 신기한 경험이다.
위 링크에서 다음과 같이 미리 의존성을 설치해서 만들 수 있다.
채팅을 위한 웹소켓과 API 문서를 쉽게 정리할 swagger 관련 의존성을 설치했다.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-websocket") // websocket
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2") // swagger
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
🏭 구조 정리
1. 방송
REST API를 받아 처리해줄 요소들이다.
a) HTTP Reuqst를 받을 Controller
@Tag(name = "방송", description = "방송을 생성, 삭제, 조회")
@RestController
@RequestMapping("/broadcast")
class BroadcastController(private val service: BroadcastService ) {
@PostMapping()
fun createBroadcast(@RequestBody broadcastDTO: BroadcastDTO): BaseDTO {
val model = broadcastDTO.toModel()
return service.createBroadcast(new = model)
}
@DeleteMapping("/delete/{id}")
fun deleteBroadcast(@PathVariable("id") id: String): BaseDTO {
return service.deleteBroadcast(id = id)
}
@GetMapping("/all")
fun getAllBroadcast(): List<Broadcast> {
return service.getAllBroadcast()
}
}
총 3개의 HTTP Method가 보인다, post, delete, get이 보인다.
post는 body에서 데이터를 받고, delete는 path파라미터로 id값을 받는다.
또한 Controller라는 실질적인 처리를 담당하는 service 객체를 갖고 있다.
b) 실질적인 처리를 담당하는 Service
@Service
class BroadcastService(private var broadcasts: HashMap<String, Broadcast>,
private var chatRooms: LinkedHashMap<String, ChatRoom>,
private val objectMapper: ObjectMapper
) {
fun createBroadcast(new: Broadcast): BaseDTO {
if (broadcasts[new.id] == null) {
broadcasts[new.id] = new
createRoom(id = new.id)
return BaseDTO(200, message = "${new.id} 방송을 시작합니다" )
}
return return BaseDTO(200, message = "이미 ${new.id} 방송이 존재합니다.")
}
fun deleteBroadcast(id: String): BaseDTO {
if (broadcasts[id] != null) {
broadcasts.remove(id)
terminateRoom(id = id)
return BaseDTO(200, message = "성공적으로 ${id} 방송이 종료됐습니다.")
}
return BaseDTO(200, message = "${id} 방송은 이미 종료되어었습니다.")
}
...
}
전체 방송정보 정보와 채팅방을 관리하며 Controller와 handler를 통해 호출되면
그에 따른 동작을 한 후 결과를 반환한다.
2. 채팅
채팅과 밀접한 컴포넌트들과 웹소켓과 관련된 내용이 들어있다.
a) Configuration
@Configuration
@EnableWebSocket
class WebSocketConfiguration(private val service: BroadcastService): WebSocketConfigurer {
private val webSocketHandler: WebSocketHandler = WebSocketHandler(service = service )
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(webSocketHandler,"/ws/chat")
.setAllowedOrigins("*")
}
}
웹 소켓과 관련 설정을 해준다. 대표적으로 handler, path, CORS에 대해 설정을 해줄 수 있다.
b) Handler
두번 째는 웹소켓에서 발생하는 여러가지 이벤트에 대한 핸들링을 담당하는 핸들러다.
@Component
public class WebSocketHandler(private val service: BroadcastService) : TextWebSocketHandler() {
private val objectMapper: ObjectMapper = ObjectMapper()
private var roomIdMapper: HashMap<WebSocketSession, String> = HashMap<WebSocketSession, String>()
override fun afterConnectionEstablished(session: WebSocketSession) {
super.afterConnectionEstablished(session)
println("접속 완료 ${session}")
}
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
val payload = message.payload
println("Paylod: ${payload}")
try {
val chatMessage = objectMapper.readValue<ChatMessage>(payload)
println("chatMessage: ${chatMessage}")
...
} catch(e: Exception) {
println(e)
}
}
override fun handleBinaryMessage(session: WebSocketSession, message: BinaryMessage) {
val byteArray = message.payload.array()
try {
val chatMessage = objectMapper.readValue<ChatMessage>(byteArray)
println("chatMessage: ${chatMessage}")
...
} catch(e: Exception) {
println(e)
}
}
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
super.afterConnectionClosed(session, status)
val roomId = roomIdMapper[session]
if (roomId != null) {
val room = service.findByRoomId(roomId)
room?.leave(session)
println("session: ${session} 이 방 ${roomId}를 떠났습니다")
}
}
}
여러 override 함수들이 있는데 세션 연결 , 세센 닫기, 그리고 2가지 handle prefix를 갖은 함수가 있는데
실질적인 값은 session으로부터 message가 왔을 때 불리는데
그 메시지 형태가 TextMessage 형태인지 BinaryMessage 형태인지 구분하는 정도이며
그 안에 핸들링하는 코드는 비슷하다.
결과
그렇다면 로컬 호스트에서 테스트를 해보자.
오른쪽 사진은 Simple Websocket Client라는 크롬 익스텐션을 통해 웹소켓 통신을 테스트 했다.
결과는 매우 성공적 .. 이제는 본격적으로 서버를 만들러가보자.
☁️ 네이버 클라우드 플랫폼 서버를 이용하여 호스팅하기
처음으로 서버를 배포하는 두근거림과 함께 부스트 캠프에서 제공해준 ncp 크레딧을 사용하러 가보자.
서버 생성하기
먼저 서버 이미지라는 것을 생성하는데 단순히 말해 어떤 OS나 하이퍼이바이저를 사용해서
호스팅 서버 컴퓨터를 만들지를 선택한다.
다음은 VPC와 Subnet을 만들고 서버에 관한 여러가지 설정들을 진행한다.
VPC와 서브넷을 생성할 때 보면 IP주소 범위를 설정하는데 가이드라인대로 위 3범위 중에서 설정하면된다.
CS 시간에 빠르게 넘어갔던 부분같은데 이번 기회에 다시 돌아보면 좋을 것 같다.
위 내용은 부스트캠프 일정이 마무리되면 조금 더 채워넣도록 하겠다.
위 과정을 잘지나면 짜잔! 서버가 성공적으로 만들어졌다.
정말 길고 긴 여정이였다... 그러면 부푼 가슴을 안고 접속해보자. 서버 콘손 접속(파란색 버튼)을 누르면
다음과 같이 접속화면이 보이게 된다.
서버 콘솔 접속과 세션시간이 뜨는데 최대 20분마다 이 화면이 꺼지게 된다..
그렇다면 실질적인 작업 시 시간제한이 있다는 뜻인데 이때 우리는 서버를
ssh를 통해 접속하여 작업을 진행야한다.
💢 SSH 억까로 날린 7시간
서버가 성공적으로 만들어지고 서버 접속 콘솔도 잘 접속 되길래 아 ssh는 저번에 했던 경험이 있어 쉽게
진행될 줄 알았다.. 제일 걱정 안한 부분에서 하루 절반을 날릴줄은 ..
ssh는 기본적으 22번 포트를 사용한다.
아래는 네이버 클라우드 플랫폼 ACG에 대한 설명이다.
간단하게 설명하면 방화벽 역할을 하며 그 규칙을 손쉽게 설정할 수 있다는 뜻이다.
우리 서버의 ACG 규칙에서 22번포트 사용에 대한 허가가 잘되어있다.
그러면 한번 시도해보자.
ssh 이용자@ip주소 -p 포트 번호
흠 ... 타임 아웃이 났다..
뭐가 문젤까 .
ping 명령어를 통해 수신여부를 확인해보자.
정말 잘 받고 있다... 이후 컴퓨터가 문제인가 하고 재부팅을 해보기도하고
서버를 삭제하고 새로운 인스턴스를 받아보기도 했다.
역시 안된다 .. 여러가지 삽질을 하다 5시간 째 바쁘게 다른 작업을 하고 있는 팀원께 접속을 요청해봤다.
아니 이럴수가 팀원은 너무 쉽게 접속이 되는거다..
아니 팀원은 접속이 잘된다.. 표본을 늘리기 위해 아는 지인분들께 부탁드려봤다.
역시 다른분들도 잘된다고한다.. 근데 여기서 하나 의심가는게 외부 카페 와이파이로도 잘된다는 말에
나는 우리집 wifi를 끊고 핸드폰 핫스팟으로 똑같이 접속을 시도해봤다
이런 ... 정말 허탈했다.. 단순히 인터넷 문제때문에 7시간을 버렸다.
나만 이런 억까를 당했나하고 살펴보니 아래 분도 같은 오류를 겪고 있다.
정리하자면 SKT 브로드 밴드를 쓰는 분들은 22번 포트를 통해 ssh 접속이 ISP쪽에서 막히는 것 같다.
그래서 위 링크의 해결법은 인터넷을 바꾸셨고 나는 포트포워딩을 통해 22번 포트를 2022번 포트로 바꿨다
아까 위에 있던 ACG에 2022번 포트를 허용해주고 다음과 같이 서버의 ssh 데몬의 config를 바꿔줬다.
vi /etc/ssh/sshd_config
이렇게 #Port 번호 아래에 원하는 Port번호를 넣고 ssh 데몬을 restart한다.
/etc/init.d/ssh restart
이후 똑같이 2022 번호로 접속을 시도해보면 잘 접속된다.
ssh root@ip주소 -p 2022
1시에 시작 서버 생성 다하고 8시에 끝난 ssh 연결.. 정말 허탈하지 않을 수 없다..
✈️ 서버에 spring을 올려 호스팅하다.
드디어 마지막 단계다.. 로컬에서 테스트 잘한 코드를 그대로 서버 pc에 옮겨 실행시켜주기만하면된다.
솔직히 이 부분이 가장 시간이 오래걸릴 것 같았지만 이 부분이 가장 금방 끝났다.
1. git을 통해 remote에 있는 코드를 다운 받는다.
서버에 내 git 계정으로 등록하고 code를 다운 받는다.
2. 코드를 빌드 한다.
./gradlew build
위 명령어를 통해 build를 진행한다.
3. 서버 실행
/build/libs 아래에 빌드 결과인 .jar 파일이 있다. 이 파일을 다음 명령어로 실행시키면된다.
nohup java -jar websock-0.0.1-SNAPSHOT.jar
nohup은 프로그램을 백그라운드로 실행한다는 리눅스 명령어다.
여기서 build부터 서버 실행까지 생각보다 복잡하다는 생각에 makefile과 쉘스크립트를 통해 간소화를 진행했다.
4. 스크립트를 통한 제어
이렇게 간소화 하니 너무 편한다 ..
🤬 소감
솔직히 작업이 끝나고 홀가분한 기분보다 SSH로 날린 7시간이 많이 아까웠다...
부스트 캠프 그룹프로젝트 중 가장 중요한 1주일에서 7시간은 정말 소중한 시간이데 ..
하지만 서버를 직접 호스팅하나 경험은 너무나 값진 경험이였다..
당장 사이드 프로젝트를 진행할 때 서버를 개발하지 못해 프로젝트의 주제나 규모가 많이 줄어들었는데
서버를 직접 배포해보면서 나만의 무기가 하나 더 생겼다.
물론 서버 자체의 퀄리티는 만족할만큼은 아니지만 만들었다는 성취감과 그 과정에서 지나갔던
CS 지식들을 다시 복습하는 계기가 된 것 같다.