실전프로젝트에 실시간 채팅과 음성 채팅 기능을 구현해야 한다.

Spring 채팅기능에 관련해 찾아보니 WebSocket과 WebRTC 두가지 키워드가 등장했다. 

다음은 WebSoket과 WebRTC에 대한 간략한 정리이다.


WebRTC vs WebSockets: What are the key differences?

  • WebSocket provides a client-server computer communication protocol, whereas WebRTC offers a peer-to-peer protocol and communication capabilities for browsers and mobile apps.
  • While WebSocket works only over TCP, WebRTC is primarily used over UDP (although it can work over TCP as well).
  • WebSocket is a better choice when data integrity is crucial, as you benefit from the underlying reliability of TCP. On the other hand, if speed is more important and losing some packets is acceptable, WebRTC over UDP is a better choice.
  • WebRTC is primarily designed for streaming audio and video content. It is possible to stream media with WebSockets too, but the WebSocket technology is better suited for transmitting text/string data using formats such as JSON.

 

TCP와 UDP의 차이점은 ?

  • TCP는 연결 지향 프로토콜인 반면 UDP는 비연결 프로토콜입니다. TCP가 UDP보다 비교적 느리기 때문에 TCP와 UDP의 주요 차이점은 속도입니다. 전반적으로 UDP는 훨씬 빠르고 간단하며 효율적인 프로토콜이지만 손실된 데이터 패킷의 재전송은 TCP를 통해서만 가능합니다. 
  • TCP와 UDP의 또 다른 주목할 만한 차이점은 TCP는 사용자에서 서버로(또는 그 반대로) 데이터를 순서대로 전달하는 반면 UDP는 종단 간 통신 전용이 아니며 수신자의 준비 상태를 확인하지도 않는다는 것입니다.  

 

When to use WebRTC?

WebRTC is a good choice for the following use cases:

  • Audio and video communications, such as video calls, video chat, video conferencing, and browser-based VoIP.
  • File sharing apps.
  • Screen sharing apps.
  • Broadcasting live events (such as sports events).
  • IoT devices (e.g., drones or baby monitors streaming live audio and video data).

 

When to use WebSockets?

We can broadly group Web Sockets use cases into two distinct categories:

  • Realtime updates, where the communication is unidirectional, and the server is streaming low-latency (and often frequent) updates to the client. Think of live score updates or alerts and notifications, to name just a few use cases.
  • Bidirectional communication, where both the client and the server send and receive messages. Examples include chat, virtual events, and virtual classrooms (the last two usually involve features like live polls, quizzes, and Q&As). WebSockets can also be used to underpin multi-user synchronized collaboration functionality, such as multiple people editing the same document simultaneously.

 

[ 정리 ]

  • WebSocket은 데이터 무결성이 중요한 경우 TCP의 기본 안정성을 활용하므로 더 나은 선택
  • 반면에 속도가 더 중요하고 일부 패킷 손실이 허용되는 경우 UDP를 통한 WebRTC가 더 나은 선택

 

[ 결론 ] : 두 가지 모두 사용 ?

WebSocket : 게임과 채팅 구현을 위해 실시간성을 보장하는 통신 방식. HTTP에서도 실시간성을 보장하는 반이중 기법들(polling, long polling, streaming)이 존재하나, 실시간성이 떨어지거나 서버 부하가 올라갈 수 있다는 단점이 있어서, 하나의 TCP 연결을 통해 양방향 통신을 제공하는 Websocket을 선택.

 

WebRTC : 화상/보이스 채팅 기능을 위해 카메라와 마이크 등의 미디어 자원을 통해 실시간으로 소통할 수 있는 기술이 필요. WebRTC는 브라우저에서 지원하는 프레임워크로 많이 사용되어 안정성이 검증되어있고, 별도의 플러그인 없이 사용이 가능하여 User 경험에 유리하다.

 

이번 프로젝트땐 아마 일반 채팅엔 WebSocket을, 그리고 화상/음성 채팅엔 WebRTC를 사용할 것 같다 : ) 


[ 참고자료 ]

https://ably.com/topic/webrtc-vs-websocket

 

WebRTC vs. WebSocket: Which one is the right choice for your use case? | Ably Realtime

We compare WebRTC with WebSocket. Discover how they are different, their pros & cons, and their use cases.

ably.com

https://www.lifesize.com/blog/tcp-vs-udp/

 

TCP vs. UDP: What’s the Difference?

In the world of Internet protocol traffic, consumers can choose between a TCP or UDP setup for their business or personal use. When it comes to TCP vs UDP features and functions, each brings its own set of advantages and challenges. With that said, UDP is

www.lifesize.com

 

 

1. 어려웠던 부분 :

드디어 오늘 나의 취업이 달린 실전프로젝트가 시작되었다. 오늘은 새로운 팀원들과 인사하고 기획하는 시간을 가졌다. 사실 서비스 기획이 가장 어려운 부부인 것 같다. 나는 공공 API를 이용해 약정보를 알려주는 ( 약의 기능, 부작용, 같이먹으면 안되는 음식 등등) 서비스와 스무고개 (게임) 아이디어를 제안했고 다른 분들은 반려견 돌봄 서비스, 건강관리 서비스, 마피아게임 등의 의견을 주셨다. 

 

오랜 회의 끝에 런닝맨에서 나왔던 양세찬게임, 이른바 "콜마이게임"을 기획하였다. 하루종일 몇시간씩 기획회의를 하고, 와이어프레임을 짜고, 디자이너님을 위한 캐릭터 레퍼런스까지 찾아보고, 담당 디자이너님을 만나 회의를 진행하다 보니 진이 다 빠지는 하루였다. 그래도 팀원 모두가 만족한 기획안이 나온 것 같아 기분이 좋다.

 

2. 느낀 점 : 

이번엔 일주일이 아니라 6주 동안 함께하는 팀인 만큼, 그라운드 룰을 정하는 시간을 가졌다. 그 중 짜증내면 반성의자로 추방, 힘들단 말 대신 아자아자 화이팅 외치기가 아주 마음에 든다 ㅎㅎ 새로운 기술을 사용하게 되는 만큼 걱정도 되지만, 그만큼 기대도 되고 재밌는 프로젝트가 될 것 같다 ! 

 

 

3. 새로 알게 된 내용 : 

오늘 기획 회의를 마치고, 우리가 이번 프로젝트에서 깊게 파보고 사용하게 될 음성채팅기능에 대해 찾아보았다. 큰 키워드로 WebSoket, WebRTC, Openvidu, Redis, RabbitMQ 등의 자료들을 볼 수있었는데 내일 더 자세히 공부해 봐야할 것 같다.

 

4. 셀프칭찬 (오늘 잘한 일) : 새로운 기술에 도전하는 나를 칭찬한다 ! ㅎㅎ 

 

5. 내일 할 일 : 실전 프로젝트에 필요한 기술 공부하기


[오늘 공부한 부분] 

 

Controller

 

//숙소 전체 조회
    @GetMapping("/rooms") // size '/api/rooms?page=0&size=3'
    public ResponseEntity<List<RoomResponseDto>> getRooms(@AuthenticationPrincipal UserDetailsImpl userDetails,
                                                          @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
                                                          @RequestParam(required = false, defaultValue = "-1") int minPrice,
                                                          @RequestParam(required = false, defaultValue = "-1") int maxPrice,
                                                          @RequestParam(required = false) String type) {
        return ResponseEntity.ok(roomService.getRooms(userDetails.getUser(), pageable, minPrice, maxPrice, type));
    }

    // 비회원 숙소 전체 조회
    @GetMapping("/rooms/main")
    public ResponseEntity<List<UnClientResponseDto>> getnoclientRooms(@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
                                                                      @RequestParam(required = false, defaultValue = "-1") int minPrice,
                                                                      @RequestParam(required = false, defaultValue = "-1") int maxPrice,
                                                                      @RequestParam(required = false) String type) {
        return ResponseEntity.ok(roomService.getnoclientRooms(pageable, minPrice, maxPrice, type));
    }

    //숙소 키워드 조회
    @GetMapping("/rooms/search") // '/api/rooms/search?keyword=제목&page=0&size=2'
    public ResponseEntity<List<UnClientResponseDto>> search(@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable,
                                                            String keyword){
        return ResponseEntity.ok(roomService.search(keyword, pageable));
    }

 

  • @PageableDefault 어노테이션으로 글작성 순서에 따라 페이지를 sorting 해준다. 기본 설정 페이지는 10개의 게시글이다.
  • @RequestParam 어노테이션을 이용해 최젓가, 최댓가, 숙소타입을 필터링 할 수 있도록 설정하였다.
  • required = false 를 적용하여 Service 단에서 다양한 필터링 경우를 if문을 통해 사용하도록 하였다.
  • defaultValue = -1 로 설정한 이유는 최젓가와 최댓가가 0원부터 시작할 수 있기 위함이다. (서비스단 if문 참조)
  • 키워드 검색 부분엔 기본 조회 기능에서 String keword 인자를 추가해 주었다.

 

Repository

 

@Repository
public interface RoomRepository extends JpaRepository<Room, Long> {
    Page<Room> findByTitleContaining(String keyword, Pageable pageable);          // 키워드 검색
    Page<Room> findByType(String type, Pageable pageable);                        // 타입별 필터링
    Page<Room> findByPriceBetween(int minPrice, int maxPrice, Pageable pageable); // 가격별 필터링
    @Query(countQuery = "select count(*) from room r where (r.price between :minPrice and :maxPrice) and r.type = :type", nativeQuery = true)
    Page<Room> findByPriceBetweenAndType(@Param("minPrice") int minPrice,
                                         @Param("maxPrice") int maxPrice,
                                         @Param("type") String type,
                                         Pageable pageable);                      // 타입+가격별 필터링
}

 

  • 키워드 검색은 Cotaining()을 사용하였다. title 컨테이닝이기 때문에 키워드검색은 제목 부분만 해당된다.
  • 타입+가격별 필터링은 Spring JPA Data 에서 제공하는 쿼리문만으로는 적용되지 않아, nativeQuery를 사용하였다. 
  • 구글링으로 찾은 블로그를 참조하여 그 중에서 countQuery를 사용하였는데, count(*) 부분의 동작 원리는 잘 모르겠다...^^
  • 기술 매니저님이  QueryDSL을 사용해 보라는 피드백을 주셨다 ! 

 

Service

 

//숙소 페이징, 필터링
    @Transactional(readOnly = true)
    public Page<Room> addFilter(Pageable pageable, int minPrice, int maxPrice, String type) {
        // pageable은 필수, type, price(기본값 -1)별 필터링
        Page<Room> roomList = roomRepository.findAll(pageable);         // RequestParam page, size만 있을 때
        if (type != null && minPrice == -1 && maxPrice == -1) {         // RequestParam type만 있을 때
            roomList = roomRepository.findByType(type, pageable);
        } else if (type == null && minPrice != -1 && maxPrice != -1) {  // RequestParam price만 있을 때
            roomList = roomRepository.findByPriceBetween(minPrice, maxPrice, pageable);
        } else if (type != null && minPrice != -1 && maxPrice != -1) {  // RequestParam type, price 둘 다 있을 때
            roomList = roomRepository.findByPriceBetweenAndType(minPrice, maxPrice, type, pageable);
        }
        return roomList;
    }

    //숙소 정보 전체 조회
    @Transactional(readOnly = true) //회원 전체 조회
    public List<RoomResponseDto> getRooms(User user, Pageable pageable, int minPrice, int maxPrice, String type) {
        List<RoomResponseDto> roomResponseDto = new ArrayList<>();
        for (Room room : addFilter(pageable, minPrice, maxPrice, type)) {
            List<String> imageFileList = new ArrayList<>();
            for (ImageFile imageFile : room.getImageFileList()) {
                imageFileList.add(imageFile.getPath());
            }
            roomResponseDto.add(new RoomResponseDto(
                    room,
                    (checkLike(room.getId(), user)),
                    imageFileList));
        }
        return roomResponseDto;
    }

    @Transactional(readOnly = true) //비회원 전체 조회
    public List<UnClientResponseDto> getnoclientRooms(Pageable pageable, int minPrice, int maxPrice, String type) {
        List<UnClientResponseDto> unClientResponseDto = new ArrayList<>();
        for (Room room : addFilter(pageable, minPrice, maxPrice, type)) {

            // path를 객체로 받아올 경우 주석부분 사용,
//            List<ImageFileResponseDto> imageFileResponseDtoList = new ArrayList<>();
//            for (ImageFile imageFile : room.getImageFileList()) {
//                imageFileResponseDtoList.add(new ImageFileResponseDto(imageFile));
//            }

            // path를 String 타입으로 받올 경우
            List<String> imageFileList = new ArrayList<>();
            for (ImageFile imageFile : room.getImageFileList()) {
                imageFileList.add(imageFile.getPath());
            }
            unClientResponseDto.add(new UnClientResponseDto(room, imageFileList));
        }
        return unClientResponseDto;
    }

    //숙소 키워드 검색
    @Transactional(readOnly = true)
    public List<UnClientResponseDto> search(String keyword, Pageable pageable) {
        Page<Room> roomList = roomRepository.findByTitleContaining(keyword, pageable);

        List<UnClientResponseDto> roomResponseDtos = new ArrayList<>();
        for (Room room : roomList) {
            List<String> imageFileList = new ArrayList<>();
            for (ImageFile imageFile : room.getImageFileList()) {
                imageFileList.add(imageFile.getPath());
            }
            roomResponseDtos.add(new UnClientResponseDto(room, imageFileList));
        }

        return roomResponseDtos;
    }

 

  • 필터링 기능은 if문을 사용하여 구현하였다.
  • 처음에 이미지파일 url (path라는 변수 사용)을 프론트쪽으로 객체타입으로 보내주었는데, String 타입이 더 가공하기 편하다고 하여 주석처리하고 List<String>을 이용하여 반환타입을 문자열로 바꿔주었다.
  • 참고로, UnClientResponseDto는 비회원 전용 Dto이다.

[ 참고자료 ]

 

 

1. 어려웠던 부분 : 오늘은 마지막까지 남겨져있던 프론트와의 작업을 끝내고, 트러블 슈팅을 팀원들과 정리하는 시간을 가졌다. 일주일이라는 시간동안 촉박하게 클론프로젝트를 구현하다보니 그때 그때 에러 처리에만 급급하고 트러블 슈팅을 정리해놓지 않아서 다시 생각해보기가 꽤나 힘들었다. 다음부턴 최대한 노트에 바로바로 메모해두고 그날 트러블슈팅을 당일에 정리하도록 해야겠다.

 

2. 느낀 점 : 힘든 한 주였지만, 걱정했던 것 보다 좋은 결과물이 나와서 뿌듯했다. 프론트엔드분들이 너무 고생이 많았던 한주였는데 끝까지 열심히 해주셔서 감사함을 느꼈다. 다른사람의 열정을 통해 동기부여도 되느 것 같다. 나도 누군가에게 동기부여가 되도록 나태해 지지말고 열심히 해야겠다.

 

3. 새로 알게 된 내용 : 서비스아키텍쳐를 만들어 보면서, 다시한번 클라이언트와 서버간의 통신 방식을 복습할 수 있었다. 그리고 그 과정에 프론트엔드에선 어떤 기술을 사용하는지, 그리고 백엔드에선 어떤 기술을 사용하는지도 정리할 수 있었다. 그리고 다시한번 느끼지만, 깃헙에 리드미작성 정말 중요한 것 같다. 한눈에 들어오게 기능/기술스택/트러블 슈팅을 잘 정리해두면 나중에 포트폴리오로 활용할 때 유용할 것 같다.

 

4. 셀프칭찬 (오늘 잘한 일) : 오늘 클론코딩 발표자료를 위해 리드미작성과 서비스 아키텍쳐 이미지파일 만드는 작업을 진행했다. 코드를 구현하는 것도 중요하지만 그 외 팀에 필요한 작업을 수행하는 것도 중요하지 않을까? 오늘 일인분 하지 않았나 싶다 ! ㅎㅎ 그리고 이번주에 궁금했던 개념들을 정리하는 시간을 가졌다. 궁금한 부분을 놓치지 않고 간단하게라도 정리해보고 넘어가려는 나의 자세 칭찬함 ㅎ

 

5. 내일 할 일 : 실전 프로젝트 발제 / 기획


[오늘 공부한 부분] 

 

[32] 이번주에 궁금했던 부분 정리

 

[32] 이번주에 궁금했던 부분 정리

Optional vs List Optional은 null 또는 값을 감싸서 NPE(NullPointerException)로부터 부담을 줄이기 위해 등장한 Wrapper 클래스이다. Optional은 값을 Wrapping하고 다시 풀고, null 일 경우에는 대체하는 함수를 호출

leejincha.tistory.com

[09] 트러블 슈팅

 

[09] 트러블 슈팅

@Enablejpaauditing 문제 게시글을 수정할 때, CreatedAt/ModifiedAt 값이 null로 반환되는 문제 해결 @Enablejpaauditing 어노테이션 추가 @EnableJpaAuditing @SpringBootApplication public class HanghaebnbApplication { public static void

leejincha.tistory.com

[10] 프로젝트 정리

 

[10] 프로젝트 정리

1. 프로젝트 소개 🏖️ 에어비앤비 웹서비스를 클로닝한 프로젝트 입니다 https://www.youtube.com/watch?v=FwMvImgOa3k 2. 주요 기능 Spring Security, JWT를 이용한 회원가입/로그인 이메일 인증을 통한 로그인

leejincha.tistory.com

 

Optional<> vs List<>

  • Optional은 null 또는 값을 감싸서 NPE(NullPointerException)로부터 부담을 줄이기 위해 등장한 Wrapper 클래스이다. 
  • Optional은 값을 Wrapping하고 다시 풀고, null 일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 잘못 사용하면 시스템 성능이 저하된다.
  • 그렇기 때문에 메소드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋다. 즉, Optional은 메소드의 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때 반환값으로만 사용되어야 한다.
  • Optional은 반환 타입으로써 에러가 발생할 수 있는 경우에 결과 없음을 명확히 드러내기 위해 만들어졌으며, Stream API와 결합되어 유연한 체이닝 api를 만들기 위해 탄생한 것이다. 예를 들어 Stream API의 findFirst()나 findAny()로 값을 찾는 경우에 어떤 것을 반환하는게 합리적일지 Java 언어를 설계하는 사람이 되어 고민해보자. 언어를 만드는 사람의 입장에서는 Null을 반환하는 것보다 값의 유무를 나타내는 객체를 반환하는 것이 합리적일 것이다. Java 언어 설계자들은 이러한 고민 끝에 Optional을 만든 것이다. 그러므로 Optional이 설계된 목적에 맞게 반환 타입으로만 사용되어야 한다.

 

https://mangkyu.tistory.com/203

http://www.tcpschool.com/java/java_stream_optional

 

Null vs isEmpty() 차이

Null

  • 인스턴스가 생성되지 않은 상태이다.
  • List 변수가 메모리에 아무런 주소값도 참조하지 않은 상태이다.
  • 변수에 아무것도 할당되지 않은 상태를 의미한다.

isEmpty

  • size() = 0
  • Java SE 1.6 이상부터 사용이 가능하다.
  • 배열이 생성되었으나, 배열안에 아무것도 없는 상태를 말한다.
  • 객체는 존재하지만, 공백이다.
  • 변수에 문자열값이 할당되었지만, 그 길이가 0임을 의미한다. 

 

※ Null string은 언제나 비어있지만, Empty string은 null이 아닐 수도 있다.

 

http vs https

  • HTTPS(https://)는 SSL(Secure Socket Layer) 인증서를 사용하는 HTTP(http://)입니다. SSL(또는 TLS) 인증서는 일반 HTTP 요청 및 응답을 암호화합니다. 따라서 HTTPS는 HTTP보다 더 안전한 보안용 프로토콜이라고 할 수 있습니다.
  • HTTP와 HTTPS의 유일한 차이점은 HTTPS를 사용한 웹 페이지를 통해 전송되는 모든 데이터는 추가적인 보안 계층이 있습니다. 이를 TLS(전송 계층 보안) 프로토콜이라고 합니다. 모든 유형의 데이터는 변경되거나 손상될 수 없는 HTTPS 사이트를 통해 전달되며 제3자로부터 보호됩니다.

 

※ HTTPS 확인 방법 : 도메인 이름 앞에 자물쇠 아이콘이 있으면 당신의 사이트는 HTTPS로 인해 안전한 것입니다.

 

지연로딩 vs 즉시로딩 (FetchType.LAZY or EAGER)

  • JPA에서는 데이터를 조회할 때 즉시 로딩(EAGER)과 지연 로딩(LAZY) 두 가지 방식이 있다.
  • 이 두 가지 방식을 간단하게 설명하면 즉시 로딩은 데이터를 조회할 때 연관된 데이터까지 한 번에 불러오는 것이고, 지연 로딩은 필요한 시점에 연관된 데이터를 불러오는 것이라고 할 수 있다.

  • 비지니스 로직 상 Member 데이터가 필요한 곳에 대부분 Team의 데이터 역시 같이 사용 할 필요가 있다면 어떨까? FetchType을 EAGER로 설정하여 항상 Member와 Team을 같이 조회해오는 것이 더 좋을 것이다.
  • Member를 사용하는 곳 대부분에서 Team 데이터가 필요하지 않다면? FetchType을 LAZY로 설정하여 Member만 조회하고, 드물게 Team이 필요할 땐 그 때 Team에 대한 쿼리를 한번 더 날려 조회하는것이 좋을 것이다.

※ 실무에서는 EAGER LOADING을 사용하지 않는 것을 권장한다. 

 

이유는?

즉시 로딩에서는 Member와 연관된 Team이 1개여서 Team을 조회하는 쿼리가 1개 나갔지만, 만약 Member를 조회하는 JPQL을 날렸는데 연관된 Team이 1000개라면? Member를 조회하는 쿼리를 하나 날렸을 뿐인데 Team을 조회하는 SQL 쿼리 1000개가 추가로 나가게 된다. 그렇기 때문에 가급적이면 기본적으로 지연 로딩을 사용하는 것이 좋다.

 

https://developer-hm.tistory.com/37

https://ahndding.tistory.com/24

 

UTF파일 인코딩

UTF-8

  • UTF는 Unicode Transformation Format의 줄임말로 전세계 모든 문자를 컴퓨터에서 표현하고 다룰 수 있도록 설계된 산업표준이다.
  • 따라서 한글도 당연하게 표현가능하다.
  • UTF-8은 문자열 집합과 인코딩 형태를 8bit단위로 한다는 의미를 가진다.
  • UTF-8은 한 글자를 표현하기 위해 1~4byte를 사용하는데 이를 가변길이 인코딩 방식이라고 한다.

ANSI

  • ANSI는 ASCII의 확장이라고 할 수 있다.
  • ANSI는 8bit로 이루어져 있어서 256개의 문자를 표현할 수 있다.
  • 하지만 ANSI로 모든 언어를 표현할 수 없어서 Code Page라는 개념이 도입되었다.
  • 각 언어별로 Code 값을 지정하고, Code마다 다른 문자열 표를 의미하도록 약속을 한 것이다.

CP949

  • CP949는 ANSI계열의 한글 인코딩 방식이다.
  • CP949는 Code Page 949의 줄임말이고, 949는 한국을 의미한다.
  • 유닉스계열의 완성형 코드 조합 인코딩방식인 EUC-KR을 확장하여 만든것이다.
  • EUC-KR로는 표현할 수 있는 문자에 한계가 있었기 때문이다.
  • CP949는 윈도우즈 계열에서 사용나온것이고, 마이크로소프트가 EUC-KR을 확장하여 만든것이라서 MS949라고 부르기도 한다.

https://velog.io/@ovan/File-Encoding

 

+ Recent posts