지난주 토요일 김선우 시니어 개발자님이 주신 개발자 취업 팁을 정리해 보자 !

 

  1. 신입을 채용하는 기준 : 1. 책임감 2. 의지, 열정 
  2. 신입을 뽑는 목적 = 잠재력을 보고 투자하겠다는 것 .
  3. 과제수행을 할거야 ( 온보딩 기간 )
  4. 중요한건 좌절하지 않는 마음 . 중꺾마
  5. 입사 후 1년은 배우는 기간, 1인분을 만들어나가는 기간
  6. 면접 들어갈 때 핸드폰 녹음을 키고 들어가서 나중에 피드백을 받도록

이력서 작성 팁

자기소개서는 별로 관심 없음 - 즉, 주저리주저리 쓸 필요 없음.

할줄 아는 능력이 중요함

  • 프로젝트에서 뭘 했는지
  • 기술스택 
  • 이력서는 PDF 기준 한 장 정도면 된다. (compact 하게 )
  • 이력서를 통해서 질문을 유도하게 ( 따라서 자신있는 것만 적기 )
  • 잘 할 수 있고, 잘 설명할 수 있는 것만 적기

 

좋은 회사 고르는 방법

  • 성장 가능성이 있는 회사
  • 배울 수 있는 사수가 있고, 코드 리뷰가 있는 회사
  • 연봉 1-2천 차이 아무것도 아님, 일단 배울 수 있는 회사로 가야함

 

면접을 보는 방법

  • 질문에 대한 대답을 하는 자리가 아님
  • 나도 면접관을 면접하는 자리, 내가 역으로 질문을 많이 해야함
  • 나에게 유리한 질문을 하기.
  • 나도 상대방을 파악한다는 느낌으로. 소개팅 하는 느낌으로

 

개발자로 살아남는 방법

  • Chatgdp 완전하지 않다.
  • 끊임없이 학습하는게 중요함. 그리고 나만의 데이터 베이스를 만들기
  • 변화가 없다고 느낄땐 이직을 하던지 변화를 줘야함.
  • 학습 == 공부 + 구현 ( 사이드 프로젝트 )
  • 태도와 마음가짐 : 나는 배우고 있다고 생각하면 그게 학습 ( 삽질 포함 )
  • 지치지않고 꾸준히 하는게 중요하다. 
  • 잠은 관짝 들어가서 자는 것이다.

 

학습 방법

  • 블로그, 스택오버플로우 : 학습이 아니다
  • 학습은 책과 강의가 학습이다.
  • 그리고 추가적으로 운영체제, 네트워크 공부도 하면 좋음

멘토님 책 추천

  • 알고리즘/인터뷰 :  알고리즘 문제해결 전략, 코딩 인터뷰 완전 분석, 면접을 위한 CS 전공지식 노트
  • 백엔드 : Java 기본서(Java의 정석, 혼자 공부하는 자바 등등...), 이펙티브 자바 3판, Head first design pattern
  • 책보다 좋은건 공식 문서 : oracle java doc
  • 프론트엔드 :  Do it! 모던 자바스크립트 프로그래밍의 정석, Do it! 리액트 모던 웹 개발 with 타입스크립트, 리액트를 다루는 기술, 모던 자바스크립트 Deep Dive

 

 

우리반 모두 좋은 개발자가 되기를 !!

 

 

  • 드디어 실전 프로젝트 발표가 끝났다. 리더님 덕분에 발표도 무사히 잘 끝났고, 오랜만에 주특기 주차 때 도와주셨던 기술 매니저님들도 보고 담임 매니저님도 만나서 반가웠다.
  • 다른 반 사람들의 발표를 보면서, 다들 99일이라는 시간 동안 정말 많이 성장하셨구나, 같은 기간 같은 교육 과정을 수료한 게 맞나? 싶을 정도로 멋있었다. 다들 각자 다른 배경에서 다른 이유로 개발을 시작했을 텐데, 끝까지 존버해서 이 과정을 버텨내신 것에 박수를 보내 드리고 싶다.
  • 프로젝트가 끝나기만을 기다렸는데, 막상 끝나고 나니 이제 본격적으로 취업 시작, 공부 시작이라는 생각이 든다. 
  • 우리반 모두 좋은 회사에서 좋은 주니어 개발자로 시작하기를 ! 

 

항해99 10기 B반

 

1. 어려웠던 부분 

  • 내일 드디어 실전 프로젝트 발표날이다. 오늘은 마지막으로 아무 문제가 없는지 테스트를 하고, 시니어 개발자님이 주신 마지막 발표 자료 피드백을 검토하는 시간을 가졌다. 그런데 문득, 리프레시 토큰 관련 코드에 오점이 있다는 것을 알게 되었다. 
  • 지난주 토요일 피드백에서 시니어님이 리프레시 토큰은 클라이언트에 보낼 필요가 없다고 하셔서 서버에서 액세스 토큰만 헤더에 실어주고 있었고 엑세스 토큰만 활용해서 토큰 재발급을 하고 있었는데, 이 로직 그대로라면 토큰 검증없이 계속해서 발급해주는 거라 보안상 말도 안되는 것이었다.
  • 다른 팀원분이 지난 토요일 피드백받을 때 녹화하신 영상본이 있어서 다시 봤는데, 영상을 다시 봐도 시니어님이 말씀하신 부분은 리프레시를 굳이 클라이언트단으로 보내지 말라는 말씀 같아서 이 부분을 다시 확인하기 위해 슬랙으로 연락을 드렸다. 그리고 아래는 다시 주신 피드백이다.

 

  • 그렇다. 리프레시 토큰도 원래 처음에 작성했던 코드처럼 클라이언트단에 보내주는게 맞았다. 대신 일반 API요청시엔 액세스 토큰을 헤더에 넣어서 요청을 보내고 토큰 재발급 API요청을 할 때만 헤더에 리프레시 토큰을 넣어주는 로직으로 수정했다.
  • 발표 하루 전까지 코드 수정중인 우리 ㅎㅎ

 

2. 배운 점 : 

  • 모호하고 헷갈리는게 있을 땐 시니어 개발자님께 도움을 요청하는 것도 좋은 방법인 것 같다.
  • 코드는 계속해서 뜯어보고 개선시키고 유지 보수 하는 것이 중요하다. 당시엔 최선인줄 알았던 코드도 며칠 후 다시 보면 허점이 있을 수 있다.   

 

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

  • 내가 맡은 리프레시 토큰을 끝까지 책임지려고 노력했다.
  • 내일 발표에 대비해 예상 질문 답변을 정리했다. 과연 협력사 분들이 와주실까 ...? 
  • 다른 반 사람들의 프로젝트가 너무 궁금하다 ! 다들 고생 많으셨습니다 !!

 

4. 내일 할 일 : 드디어 실전 프로젝트 발표! 

동기화란 ? 
프로세스 또는 스레드들이 수행되는 시점을 조절하여 서로가 알고 있는 정보가 일치하는 것을 의미

 

우리가 알다시피 컴퓨터는 프로세스 또는 스레드로 작업을 실행합니다. 

 

예를 들어  a = 2 라는 자원이 있다고 해보겠습니다.

P1이라는 프로세스는 a라는 값을 이용해서 어떠한 로직을 수행합니다.

그런데 만약 그 사이 P2라는 프로세스가 a의 값을 3으로 바꾸게 된다면 어떻게 될까요?

아마 프로그램은 우리가 원하는 형태의 값을 반환하지 않을 것입니다.

 

이때 우리는 a를 공유 자원이라고 말하고,

이렇게 두개 이상의 프로세스 혹은 스레드가 동기화 없이 접근하려는 현상을 race condition(경쟁 상태)라고 합니다.

그리고 이러한 문제를 해결하는 것. 즉 서로가 알고있는 정보를 일치시키는 것을 synchronization(동기화)라고 합니다.

 

참고 자료 : https://ooeunz.tistory.com/94

 


백엔드팀은 WebRTC를 위한 시그널링 서버 부분으로 요청타입에 따라 분기 처리하는 SignalHandler 클래스를 구현했다. 

그런데, 시그널링 과정에서 비정상적으로 웹소켓 연결이 끊기는 이슈가 발생했다. 기술 멘토링에서 시니어 개발자 님도 피드백을 주셨던 부분인데, 근본적으로 웹소켓이 끊기는 이유를 찾아야 한다고 하셨다. 그런데, 도무지 모르겠다. ㅠ 

 

왜 끊기는지 우리도 몰루 ... ㅠ ㅠ알려주세요 ... ! 

 

그래서 구글링을 통해 임시 방편으로 동기화 키워드인 synchronized에 대해 알게 되었다.

아래 첨부된 코드와 같이 sendMessege 메소드 자체에 스레드를 동기화 시켜주는 synchronized를 걸어주었다. 

이 방법은 성능을 많이 저하 시키기 때문에 최대한 지양해야 하는 방법이다. 그렇지만 다른 방법 모두 실패했기 때문에, 일단 사용! 

 


// 기능 : 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());
                            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:

                    log.info("======================================== DEFAULT");
                    log.info("============== 들어온 타입 : " + message.getType());
            }
        } catch (JsonProcessingException e) {
            log.info("=================== SignalHandler Json처리 에러 : {} ", e.getMessage());
        }
    }

    // 웹소켓 연결이 끊어지면 실행되는 메소드
    @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());
        }
        Member member = repositoryService.findMemberByNickname(nickname);
        List<GameRoomAttendee> gameRoomAttendeeList = repositoryService.findAttendeeByRoomId(roomId);
        for(GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            if(nickname.equals(gameRoomAttendee.getMemberNickname())){
                gameRoomService.roomExit(roomId, member);
            }
        }
    }

    // 메세지 발송
    private synchronized void sendMessage(WebSocketSession session, WebSocketResponseMessage message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            session.sendMessage(new TextMessage(json));
        } catch (IOException e) {
            log.info("============== 발생한 에러 메세지: {}", e.getMessage());
        }
    }
}

 

오늘도 이렇게 하루가 갔습니다.

 

1. 어려웠던 부분 

  • 없다 ! 오늘은 프론트엔드분들과 디자이너님의 UI변경을 제외한곤 백엔드 팀은 급하게 처리해야할 일은 없었다.  ( 뭔가 한가한게 싸늘하다. 쉴새없이 달려왔는데, 이렇게 갑자기 하루가 여유가 있으니까 이상하다. )

 

2. 느낀 점 : 

  • 오늘은 여유가 있어서 유튜브로 개발자 브이로그, 개발자 면접, 개발자 현실 등 다양한 컨텐츠들을 찾아봤다. 
  • 수료를 코앞에 두고 요즘들어 정말 내가 개발자가 될 수 있는 사람인가에 대한 고민이 많아졌다. 그래서 자꾸만 위와 같은 영상을 찾아보면서 스스로를 객관적으로 판단해 보려 한다. 항해를 하면서 아 이사람은 진짜 개발 머리가 타고났다 라고 생각드는 사람도 있는데, 일단 나는 아니다. 언젠가 같은반 팀원분이 나는 '노력형 천재'라고 정말 기분좋은 칭찬을 해주신 적이 있는데, 진짜 쥐어짜내면서 노력해야 그나마 따라가는 사람이다. 
  • 빨리 부트 캠프가 끝나기만을 기다렸는데, 진짜 일주일도 안남은 오늘 ! 가끔 급격하게 블로그 방문자 수가 느는 날이 있는데 오늘도 갑자기 방문자 수가 100명을 돌파했다. 아마 항해99 키워드로 검색하신 분들인 것 같은데, 다음주에 수료를 마치면 항해99 리얼 후기를 남겨야 겠다. 

 

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

  • 오늘 PM직무로 한 군데 지원서를 넣었다. 스스로를 의심하지 말자

 

5. 내일 할 일 : 내일은 오후에 만나기로 해서 아침에 운동갔다가 오후에 발표관련 공부를 할 예정 ! 예상 질문을 좀 작성해봐야 겠다.

+ Recent posts