생일을 축하해 주셔서 감사합니다 : ) !

 

 

1. 어려웠던 부분 

  • 토요일은 기술 멘토링이 있는 날이다. 현직에 계시는 시니어님께 웹소켓 세션이 끊어지는 이슈에 대해 여쭤보고 synchronized 키워드로 막아놨다고 말씀드렸더니 잘못된 방식일 것 같다는 피드백을 주셨다. 일단 먼저 왜 웹소켓이 끊어지는이 이유를 찾아야하고 synchronized 키워드 사용은 최대한 지양해야 한다고 말씀해 주셨다. 어렵다.

 

2. 느낀 점 : 

  • 현직 시니어님의 피드백을 받을때마다 CS지식이 정말 많이 부족함을 느낀다. 좀 더 잘 이해하고 더 구글링을 잘 하기 위해선 기본 지식이 필요한데, 지금까지 항해99 과정을 따라오면서 실무적인 부분, 과제구현에 급급하느라 많은 부분의 공부를 놓치고 있지 않았나 라는 생각이 들었다. 대학에서 4년을 배워도 부족하다고 할텐데 이 많은 지식을 3개월 - 4개월 만에 흡수하려니 쉽지가 않다.

 

3. 새로 알게 된 내용 :

  • synchronized 키워드 사용은 최대한 지양해야 한다.
  • interface와 class 의 차이를 확실히 알아야 한다. ( Map은 interface 이다. )
  • Stomp는 메세지브로커가 아니다. 이 역할에 대해 제대로 공부해놓기.

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 지난 크리스마스도, 새해도, 설날도 그리고 오늘 내 생일도 항해99를 하며 보냈다. 이 시기가 지나고 나면 개발자가 되지 못하더라도 후회는 없을 것 같다. 마지막까지 후회 없이 잘 버티고 열심히 공부하자 ! 

 

5. 내일 할 일 : 푹 쉬기 ! 


[오늘 공부한 부분] 

 

1. Interface vs Class

  추상 클래스 인터페이스
사용 키워드 abstract interface
사용 가능 변수 제한 없음 static final (상수)
사용 가능 접근 제어자 제한 없음 (public, private, protected, default) public
사용 가능 메소드 제한 없음 abstract method, default method, static method, private method
상속 키워드 extends implements
다중 상속 가능 여부 불가능 가능 (클래스에 다중 구현, 인터페이스 끼리 다중 상속)
공통점 1. 추상 메소드를 가지고 있어야 한다.

2. 인스턴스화 할 수 없다 (new 생성자 사용 X)

3. 인터페이스 혹은 추상 클래스를 상속받아 구현한 구현체의 인스턴스를 사용해야 한다.

4. 인터페이스와 추상클래스를 구현, 상속한 클래스는 추상 메소드를 반드시 구현하여야 한다.

[ 참고 자료 ]

 

[JAVA] ☕ 인터페이스 vs 추상클래스 차이점 - 완벽 이해하기

인터페이스 vs 추상클래스 비교 이 글을 찾아보는 독자분들은 아마도 이미 인터페이스와 추상클래스 개념을 학습한 뒤에 이 둘에 대하여 차이의 모호함 때문에 방문 했겠지만, 그래도 다시한번

inpa.tistory.com

 

 

 

 

 

1. 어려웠던 부분 

  • 팀원분의 지인이 카카오 현직 개발자로 계셔서 우리 깃헙 코드를 보고 아래와 같은 피드백을 주셨다. 그래서 오늘은 이부분들을 하나씩 해결하는 빡쎈 하루였다. 
    1. 업적 기능이 과연 기술적으로 꼭 들어가야하는 기능인지
    2. Optional.get() 안티패턴이니 이렇게 쓸거면 사용하지 말 것
    3. GameRoom에서 status가 왜 스트링인지, -> boolean 타입으로 바꿀 것
    4. HashMap -> Map으로 수정, 항상 상위타입으로 쓰는 습관 들일 것
    5. 이미지 업로드 동작 방식을 아는지 
    6. ResponseEntity<?> 쓰지 말것, 항상 타입은 명확하게 -> 이유는 찾아보라고 함
    7. 정적 타입의 장점 찾아볼것, 타입 안정성도
    8. throws IOException 무작정 던지지 말고 예외처리 하고 던져야됨
    9. Stomp 동작 방식 명확하게 이해하고 있어야됨, 웹소켓이랑 연관이 없는 기능이다
    10. 메세지 브로커는 왜 내장 브로커 사용했는지, 왜 쓰면 안 되는지
    11. LettuceConnectionFactory 뭔지 공부
    12. 아마존 S3 경로 아마존 주소 안 나오게 수정해야됨
    13. 로그인 기능 컨트롤러 -> 서비스 -> 컨트롤러로 되어 있는데 컨트롤러까지 안 가고 Spring Security단에서 끝낼 수 있음
  • 그리고 오늘 세션이 랜덤하게 끊어지는 부분을 synchronized키워드를 사용하여 스레드 간 동기화를 시켜주는 방법으로 막아보았다. 일단 돌아가는 것 같기는 한데 정답이 맞는지는 몰라서 이번주 토요일 피드백 시간에 시니어분께 여쭤보려 한다.

2. 느낀 점 : 

  • 갈수록 어렵다 !!!! 

 

3. 새로 알게 된 내용 :

  • 팀원분들께 내가 맡은 Refresh Token 관련 코드리뷰를 해드려야 하는데, 코드 대부분을 그냥 냅다 긁어다 썼다보니 Jwt, Spring Security 에 관한 이해가 너무 없어서 한줄 한줄 메소드가 어떤 역할을 하는지 정리하는 시간을 가져보았다. 오늘 공부한 부분 링크 참조 ! 

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 새로 알게된 부분은 최대한 블로그에 적으려고 했다. 다른 팀원분이 나에게 매일 블로그를 기록하는 것이 대단하다고 칭찬해 주셨다. 그래 내가 내세울 수 있는 건 꾸준함인 것 같다.

 

5. 내일 할 일 : Refresh Token TTL / CrudRepository 적용


[오늘 공부한 부분] 

 

[53] 트러블 슈팅 : Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID

 

[53] 트러블 슈팅 : Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID

1. 문제 상황 포스트맨으로 테스트를 했을 땐 잘 들어 가는데, 프론트에서 게시글 조회 하면 조회하는 이미지가 자꾸 엑박이 뜨고 아래와 같은 오류 메세지가 콘솔창에 떴다. Failed to load: resource:

leejincha.tistory.com

[23] Refresh Token 코드 리뷰 (1)

 

[23] Refresh Token 코드 리뷰 (1)

1. JwtUtil @Slf4j @Component @RequiredArgsConstructor public class JwtUtil { private final RefreshTokenRepository refreshTokenRepository; private final UserDetailsServiceImpl userDetailsService; private static final String BEARER_PREFIX = "Bearer "; privat

leejincha.tistory.com

[24] Refresh Token 코드 리뷰 (2)

 

[24] Refresh Token 코드 리뷰 (2)

1. RefreshToken (Entity) @Getter @Setter public class RefreshToken implements Serializable { private static final long serialVersionUID = 6494678977089006639L; private String refreshToken; private String email; public RefreshToken(String token, String emai

leejincha.tistory.com

 

 

 

1. 어려웠던 부분 

  • 오늘 Redis에 저장하고 있는 Refresh Token 유효기간을 설정하는 코드를 작성했다. 구글링을 통해 레디스의 TTL(TimeToLive) 기능을 활용하면 유효시간이 지났을 때 자동삭제된다고 보았는데, 코드를 작성해도 DB에서 삭제 되지 않았다. 결국 RedisTemplate 오퍼레인 사용하는 방법으로 해결하긴 했는데, 다른조 분에게 물어보니 아마 CrudRepository를 이요해야 TTL기능을 사용할 수 있는 것 같다는 이야기를 들었다. 내일 이부분을 다시 수정해 봐야겠다.
  • 커뮤티니 게시판에 AWS S3를 활용하여 이미지 파일을 저장하고 불러오고 있는데, 포스트맨에 나오는 이미지 url과 실제 S3에 저장되는 url이 일치하지 않아 이미지르 불러오지 못하는 오류가 발생했다. 팀원들과 오랜 삽질 끝에 오늘도 역시 해결사 소영님이 해결해 주셨다. 이유는 우리 버켓이름이 namoldak.com 으로 설정되어 있었는데 버켓에 "."이 들어가서 제대로 인식하지 못했던 것이다 ! 

 

2. 느낀 점 : 

  • 문제를 척척 해결하는 팀원 분들을 보며, 난 정말 부족한게 많다는 생각을 했다. 부럽기도하고 또 한편으론, 난 코딩에 소질이 없는 것 같다는 생각도 들었다. 

 

3. 새로 알게 된 내용 :

  • Redis에서 만료 시간을 설정하는 방법은 RedisTemplate 오퍼레이션 사용시 만료 시간을 설정하는 법과 CrudRepository를 이용하여 저장할 객체에 timeToLive를 설정하는 방법 두 가지가 있는데, RedisTemplate만을 이용하면서 객체에 timeToLive를 적용해 제대로 적용되지 않는다. 제대로 적용하려면 CrudRepository를 사용해 timeToLive를 사용해야 한다.

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 매일 하나씩 꼭 있는 트러블 슈팅을 통해 새로운 것들을 배우고 있다. 오늘도 트러블 슈팅을 통해 두가지나 알게 되었다 !

 

5. 내일 할 일 : 피드백 반영해서 코드 수정하기, 시그널링 서버에서 여러 session 충돌로 연결이 끊어지는 이슈 해결하기


[오늘 공부한 부분] 

 

[22] ubuntu 서버에 Redis 설치하기

현재 구현하고 있는 서비스에 Redis를 사용하고 있기 때문에 사용하는 ubuntu 서버에 Redis를 설치해 줘야만 애플리케이션이 제대로 동작한다. 설치 순서는 아래와 같다 1. ubuntu 서버에 아래 redis 설

leejincha.tistory.com

 

1. 어려웠던 부분 

  • 우리팀은 계속해서 CD 과정에 오류를 겪고 있었다. 우리는 application-local.properties 파일에 깃헙이나 외부에 노출되면 안되는 정보들을 담아주고 있고, 이 파일을 깃헙에서 Secrets 파일로 설정해 놓았다. 

  • 그런데, CD를 하는 과정에서 계속 이 properties 파일이 포함되지 않고 빠지는 오류가 있었다. 이 부분을 우리팀 트러블슈팅 해결사 소영님이 해결해 주셨다. 아래는 우리 프로젝트의 gradle.yml파일 일부분이다.
  • 오류가 났던 경우엔 키 정보 있는 properties 부분이 #build 부분 아래에 위치하고 있었다. 즉, 빌드를 다 하고 나서야 암호화된 부분이 작동하기 때문에 빌드에 반영이 되지 않았던 것이다.
      # 키 정보 있는 properties 따로 관리
      - name: make application.properties
        if: true
        run: |
          cd ./src/main/resources
          touch ./application.properties
          echo "${{ secrets.PROPERTIES }}" > ./application.properties
        shell: bash

      # gradlew 권한 설정
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

      # build
      - name: Build with Gradle
        run: ./gradlew clean build -x test
        shell: bash
  • 아래는 소영님 블로그에서 발췌한 부분이다. 

📍gradle.yml 수정 (메인이되는 yml 파일)

yml 파일에서 jobs > steps 부분에서 아래 코드를 추가해야하는 데 반드시 build과정 전에! 이 코드가 있어야한다. 

깃헙 프로젝트 배포 과정에서 application-secret.properties를 포함시켜준 뒤 빌드를 하고 그 jar를 복사해 EC2에서 실행해야 하기 때문. 빌드 뒤에 들어가버리면 jar파일에서 빠져버려 정상 작동하지 않는다.

 

2. 느낀 점 : 

  • 드디어 CD 까지 성공하였다 ! 일주일 내내 고생하신 팀원분들에 너무 감사하다. 항해99 솔직히, 부트캠프에서 제공해주는 자료를 통해 배우는 것보다 팀원분들과 반 분들에게 배운게 더 많은 것 같다. 

 

3. 새로 알게 된 내용 :

  • gradle.yml 파일의 경우, 프로젝트마다 customized 되기 때문에 구글링을 통해 코드를 긁어오기보다 맡은 프로젝트의 성격에 맞게 새로  추가되는 부분이 많았다. 주의할 점은 이 파일의 코드는 위에서부터 아래로 실행 된다는 것이다. 따라서 순서를 유의해서 작성해야 한다.

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 오늘도 부족한 나는 천천히 내 속도에 맞춰서 앞으로 가는 중!

 

5. 내일 할 일 : 리프레시 토큰 유효기간 설정하는 코드 추가하기


[오늘 공부한 부분]

 

[20] Spring CI/CD (2) CI 실전편

 

[20] Spring CI/CD (2) CI 실전편

지난 번에 개념을 공부했으니 오늘은 Github을 이용하여 CI를 구현하는 방법을 포스팅 : ) 미래의 나를 위한 포스팅이다. 1. Actions -> new workflow -> Java with Gradle [configure] 클릭 2. Edit new file 에 코드 추

leejincha.tistory.com

 

1. 어려웠던 부분 

  • 오늘 백엔드팀은 프로젝트에 관하여 급하게 처리할 사항이 없어서 코드리뷰의 시간을 가졌다. S3는 이전 프로젝트에서 사용해 봤기 때문에 대략적으로 흐름을 알고 있었지만, 웹소켓과 웹알티씨 관련된 부분은 코드를 보아도 이해가 잘 되지 않았다. 
  • 그래도 담당하신 팀원분이 차근히 설명을 해주셔서 대략적으로 이게 어떻게 돌아가는지는 알 것 같았다. 

 

2. 느낀 점 : 

  • 뷰작업에 한창인 프론트엔드와 달리 백엔드는 비교적 일찍 작업이 마무리된다. 덕분에 내가 구현하지 않은 부분을 더 공부할 수 있다. 코드를 많이 봐야 한다고 들었는데, 코드를 보면 볼수록 이 컴퓨터의 세계는 정말 신선하다. 알파벳 몇자로 애플리케이션이 만들어지는 것, 그리고 애플리케이션이 우리의 생활을 더 편리하게 해줄 수 있는 것, 내가 지금까지 살면서 한번도 궁금해하지 않고 알지 못한 세계이다. 좋은 개발자가 되서 사회에 도움이 되는 프로그램을 만들고 싶다.

 

3. 새로 알게 된 내용 :

  • 아래 코드는 웹소켓 연결과 관련된 코드이다.
// 기능 : WebRTC를 위한 시그널링 서버 부분으로 요청타입에 따라 분기 처리
@Slf4j
@Component
public class SignalHandler extends TextWebSocketHandler {

    @Autowired
    private GameRoomService gameRoomService;
    @Autowired
    private RepositoryService repositoryService;
    private final SessionRepository sessionRepository = SessionRepository.getInstance();  // 세션 데이터 저장소
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final String MSG_TYPE_JOIN_ROOM = "join_room";
    private static final String MSG_TYPE_OFFER = "offer";
    private static final String MSG_TYPE_ANSWER = "answer";
    private static final String MSG_TYPE_CANDIDATE = "candidate";

    @Override
    public void afterConnectionEstablished(final WebSocketSession session) {
        // 웹소켓이 연결되면 실행되는 메소드
    }

    // 시그널링 처리 메소드
    @Override
    protected void handleTextMessage(final WebSocketSession session, final TextMessage textMessage) {

        try {
            WebSocketMessage message = objectMapper.readValue(textMessage.getPayload(), WebSocketMessage.class);
            String userName = message.getSender();
            Long roomId = message.getRoomId();

            switch (message.getType()) {
                // 처음 입장
                case MSG_TYPE_JOIN_ROOM:

                    if (sessionRepository.hasRoom(roomId)) {
                        // 해당 챗룸이 존재하면
                        // 세션 저장 1) : 게임방 안의 session List에 새로운 Client session정보를 저장
                        sessionRepository.addClient(roomId, session);
                    } else {
                        // 해당 챗룸이 존재하지 않으면
                        // 세션 저장 1) : 새로운 게임방 정보와 새로운 Client session정보를 저장
                        sessionRepository.addClientInNewRoom(roomId, session);
                    }

                    // 세션 저장 2) : 이 세션이 어느 방에 들어가 있는지 저장
                    sessionRepository.saveRoomIdToSession(session, roomId);

                    // 세션 저장 3) : 방 안에 닉네임들 저장
                    sessionRepository.addNicknameInRoom(session.getId(), message.getNickname());

                    Map<String, WebSocketSession> joinClientList = sessionRepository.getClientList(roomId);

                    // 방안 참가자 중 자신을 제외한 나머지 사람들의 Session ID를 List로 저장
                    List<String> exportSessionList = new ArrayList<>();
                    for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
                        if (entry.getValue() != session) {
                            exportSessionList.add(entry.getKey());
                        }
                    }

                    Map<String, String> exportNicknameList = new HashMap<>();
                    for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
                        if (entry.getValue() != session) {
                            exportNicknameList.put(entry.getKey(), sessionRepository.getNicknameInRoom(entry.getKey()));
                        }
                    }

                    // 접속한 본인에게 방안 참가자들 정보를 전송
                    sendMessage(session,
                            new WebSocketResponseMessage().builder()
                                    .type("all_users")
                                    .sender(userName)
                                    .data(message.getData())
                                    .allUsers(exportSessionList)
                                    .allUsersNickNames(exportNicknameList)
                                    .candidate(message.getCandidate())
                                    .sdp(message.getSdp())
                                    .build());

                    break;

                case MSG_TYPE_OFFER:
                case MSG_TYPE_ANSWER:
                case MSG_TYPE_CANDIDATE:

                    if (sessionRepository.hasRoom(roomId)) {
                        Map<String, WebSocketSession> oacClientList = sessionRepository.getClientList(roomId);

                        if (oacClientList.containsKey(message.getReceiver())) {
                            WebSocketSession ws = oacClientList.get(message.getReceiver());
                            if(!ws.isOpen()){
                            }
                            sendMessage(ws,
                                    new WebSocketResponseMessage().builder()
                                            .type(message.getType())
                                            .sender(session.getId())            // 보낸사람 session Id
                                            .senderNickName(message.getNickname())
                                            .receiver(message.getReceiver())    // 받을사람 session Id
                                            .data(message.getData())
                                            .offer(message.getOffer())
                                            .answer(message.getAnswer())
                                            .candidate(message.getCandidate())
                                            .sdp(message.getSdp())
                                            .build());
                        }
                    } else {
                        throw new CustomException(CHAT_ROOM_NOT_FOUND);
                    }
                    break;

                default:

            }
        } catch (JsonProcessingException e) {
        } catch (Exception e) {
        }
    }

    // 웹소켓 연결이 끊어지면 실행되는 메소드
    @Override
    public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) {
        String nickname = sessionRepository.getNicknameInRoom(session.getId());
        // 끊어진 세션이 어느방에 있었는지 조회
        Long roomId = sessionRepository.getRoomId(session);

        // 1) 방 참가자들 세션 정보들 사이에서 삭제
        sessionRepository.deleteClient(roomId, session);

        // 2) 별도 해당 참가자 세션 정보도 삭제
        sessionRepository.deleteRoomIdToSession(session);

        // 3) 별도 해당 닉네임 리스트에서도 삭제
        sessionRepository.deleteNicknameInRoom(session.getId());

        // 본인 제외 모두에게 전달
        for(Map.Entry<String, WebSocketSession> oneClient : sessionRepository.getClientList(roomId).entrySet()){
            sendMessage(oneClient.getValue(),
                    new WebSocketResponseMessage().builder()
                            .type("leave")
                            .sender(session.getId())
                            .receiver(oneClient.getKey())
                            .build());
        }
        Optional<Member> member = repositoryService.findMemberByNickname(nickname);
        List<GameRoomAttendee> gameRoomAttendeeList = repositoryService.findAttendeeByRoomId(roomId);
        for(GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            if(nickname.equals(gameRoomAttendee.getMemberNickname())){
            	// 브라우저를 강제로 닫을 경우 종료시키는 웹소켓 연결 끊기
                gameRoomService.roomExit(roomId, member.get());
            }
        }
    }

    // 메세지 발송
    private void sendMessage(WebSocketSession session, WebSocketResponseMessage message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            synchronized (session){
                session.sendMessage(new TextMessage(json));
            }
        }
        catch (IOException e) {
        }
    }
}
  • 내가 이해한 바로는, 백엔드에서는 시그널링 서버 역할을 하는 코드를 구현해준다. 웹소켓연결을 할 때, 제일 처음엔 HTTP통신으로 한번 클라이언트끼리 연결을 해줘야 하는데 이때 사용된다. 한번 연결된 클라이언트는 이 이후엔 서버없이 서로 연결이 되어 통신을 한다.
  • 프론트엔드 팀원분의 요청으로 MSG_TYPE_JOIN_ROOM, MSG_TYPE_OFFER, MSG_TYPE_ANSWER, MSG_TYPE_CANDIDATE 라는 타입을 만들어서 차례로 프론트에서 이러한 타입들로 들어오는 요청을 그대로 반환해 주는 코드이다. 
  • 백엔드 코드만 봐선 사실 이해가 잘 되지 않는다. 아래 팀원분의 블로그를 참고해서 흐름을 좀 더 쉽게 이해했다 ! 
  • https://littlezero48.tistory.com/260
 

WebRTC 시그널링 서버 (Spring Boot로 구현)

📌 이 기술을 공부한 계기! 항해99 실전 프로젝트에서 게임 사이트를 제작하면서 화상 및 음성 채팅이 필요했고 우리는 WebRTC를 채택했다. 그런데 구현하는 과정에서 백엔드 입장에서 구현해야

littlezero48.tistory.com

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 다른 팀원들의 코드를 이해하기위해 노력했다. 이렇게 시간이 남을 때 풀어지지 않기 위해 계속 우테코 영상도 찾아보고 개념공부를 하려고 노력하고 있다. 존버하는 코린이 

 

5. 내일 할 일 : 서비스 배포 전 마케팅 회의 / 트러블슈팅 정리 / CD 꼭 성공하기 ! 


[오늘 공부한 부분] 

  • 코드리뷰

+ Recent posts