반응형

 생각만해도 한숨이 턱~ 나오는 나의 인생 첫 기술면접... 컨디션 관리도 제대로 못하고, 너무 긴장한 탓에 쉬운 질문도 이해되지 않았다. 어두컴컴했던 인터뷰를 복기하고나니 내가 개선해야할 점을 아주 확실하게! 찾아낼 수 있었다. 장담컨데 다음 기술면접은 이렇게 쭈구리처럼 보지 않을 것 같다! (기업 명은 비밀!)

 


1. 면접 전날...

 면접 전날부터 심장이 벌렁벌렁거렸다. 생애 처음 넣은 이력서가 서류 합격, 과제테스트 합격, 기술면접까지 가리라곤 생각지 못했기 때문이다. 물론 현업 경력은 있지만 해당 기업은 학교에서 연결해주던 곳이었고, 정석적인 채용 절차를 거치지 않았었다. 어쨌든 두근거리는 마음과 함께 과제 테스트로 제출한 코드를 복기하며 코드 리뷰를 준비하는데 시간을 쏟아부었다. 

 

 거의 3주? 정도 전에 완성했던 코드라 복기를 하는데 시간이 조금 걸렸다. 새벽에 준비를 끝마치고 오후 5시경에 있을 기술면접을 생각하며 잠을 청하려고 했다. 그런데... 나의 머가리는 잠을 거부했다. 마치 초등학교 5학년 첫 수학여행을 가기 전날의 두근거림이었다. 머릿속으로 온갖 면접 시뮬레이션을 돌리다보니 어느덧 아침이됐다. @_@;

 

2. 면접 당일

 아침밥을 먹고 면접 준비를 했다. 화상면접이라 마이크, 카메라, 목소리 등을 체크했다. 면접관님들의 눈을 고려하여 초록색 가상 배경도 준비했다. 그런데 너무 일찍부터 면접 준비를 했다. 만약 이게 대면 면접이었다면 거의 10시간 전에 면접장소에 도착한 격이었다. (홀리몰리) 시간이 많이 남아 자기 소개 연습을 하고 코드를 복기했다.  

 

3. 면접 5시간 전

 점심밥을 억지로 먹고 다시 화상 카메라 앞에 앉았다. 면접까지 약 5시간 남았었다. 이때부터였나 눈이 뻑뻑해지고 반쯤 감기기 시작했다. 하지만 여기서 잠을 자버리면 나의 첫 기술면접을 꿈에서 볼것같은 예감이 강하게 들었다. 혹시나 이런 생각 도중에 잠이 들어버릴까 겁나 박카스를 사러 편의점으로 뛰어갔다.

 

4. 면접 30분 전

 박카스를 원샷했다. 내 마지막 발악이었던것 같다.

 

5. 면접

 면접을 봤다... 결론부터 말하면 면접관님들께 너무 너무 죄송스러웠다. 그 분들도 나라는 사람을 평가하기  위해 시간을 낸것인데, 그 시간이 무색해질 만큼 대답을 하지 못했다. 면접이 끝나자마자 긴장이 풀림과 동시에 잠이 들었다.

 

 다음날 면접에 나온 질문들을 복기해보며 면접관 님들이 어떤 의도로 질문했는지, 나는 어떻게 답변하는게 좋았을지를 정리해보았다. 솔직히 정말 솔직히 질문은 어렵지 않았다. 아마 나의 답변수준을 보고 질문 수준이 같이 낮아진것 같긴 하지만.. 어쨌든 어렵지 않은 질문을 이해하지 못한 것과 그때 내 생각을 자신감 있게 전달하지 못한 점. 무엇보다 내가 가진 지식에 대한 확신을 가지지 못한 점이 너무 후회스러웠다. 

 

6. 질문 내용

Q1. userService.getUserInfo 메서드를 테스트하고 assertThat으로 검증하고 있으며, userRepository.findById 리턴 값을 USER_FOR_VALID_USER_ID 로 목킹하고 있다. assertThat 구문으로 동일한(?) 값을 굳이 검증한 이유가 뭔가?

@DisplayName("유효한 ID에 대한 유저 정보 조회")
    @Test
    void getUserInfoWithValidUserId(){
        given(userRepository.findById(any(String.class)))
                .willReturn(Optional.of(USER_FOR_VALID_USER_ID));

        given(aes256.decrypt(any(String.class)))
                .willReturn(AES256_DEC_REG_NO);

        UserDto.Info userInfo = userService.getUserInfo(VALID_USER_ID);

        assertThat(userInfo.getUserId()).isEqualTo(USER_INFO_FOR_VALID_USER_ID.getUserId());
        assertThat(userInfo.getName()).isEqualTo(USER_INFO_FOR_VALID_USER_ID.getName());
        assertThat(userInfo.getRegNo()).isEqualTo(USER_INFO_FOR_VALID_USER_ID.getRegNo());
    }

 

그때 당시 질문를 이해하지 못해 진정한 개소리를 했다. 트루 개소리였다. 되돌아보니 면접관님께서 USER_FOR_VALID_USER_ID와 USER_INFO_FOR_VALID_USER_ID를 동일한 인스턴스로 착각하고 물어보신것 같다. 변수명이 비슷해서 나도 착각했기 때문이다.

 

어찌됐든 면접관님의 질문 의도는 “userRepositroy에서 A를 리턴하도록 목킹하고, 서비스 클래스에서는 이를 그대로 리턴하는 것 같은데 테스트 코드에서 A의 필드에 대해 따로따로 검증한 이유가 뭐냐. 그냥 A 인스턴스 자체를 체크하면 되지 않냐” 였던 것 같다.

 

# 돌아갈 수 있다면...

USER_FOR_VALID_USER_ID는 엔티티이며, USER_INFO_FOR_VALID_USER_ID는 해당 엔티티를 통해 최종적으로 리턴되는 유저 정보 DTO 클래스입니다. UserService.getUserInfo 메서드에서는 조회된 엔티티에 대해 주민등록번호 복호화, 마스킹처리, DTO 변환 작업을 수행합니다. 이에 대한 응답 테스트 픽스처를 USER_INFO_FOR_VALID_USER_ID로 정의했으며, 이 값들을 비교하기 위해 각 필드에 대해assertThat 을 사용하여 체크하였습니다.

 


Q2-1. aes256을 빈으로 등록하고 있는데, 혹시 빈으로 등록하는 본인만의 기준이 있는지

 

 AES256 암복호화 클래스는 빈으로, SHA256 암호화 클래스는 일반 클래스로 두고, 정적 스태틱 메서드로 사용하고 있었다. 어쨌든 성격이 비슷한 이런 클래스들을 빈, 일반 클래스로 구분지어 사용하는 이유를 궁금해하셨던 것 같다.

 

 AES를 빈으로 사용한 이유는 주저리 주저리 답변은 한것 같지만, 클래스를 빈으로 등록하는 본인만의 기준이 있냐는 질문과 확장성에 대한 답변을 못했다. 생각해본적이 없는 내용이었다. 정신이 말짱했어도 이건 대답을 못했을 것 같다.

 

 스프링 IoC와 DI 개념을 다시 훑어보니 질문의 의도가 스프링의 기본 개념을 알고있는냐로 이해됐다. 클래스를 빈으로 등록하면 스프링 IoC 컨테이너에 의해 기본 싱글톤으로 관리되며, 이렇게 관리되는 빈들은 런타임 시 스프링에서 제공하는 여러 기능을 사용할 수 있다.

 

 생성자 메서드만 만들어 놓으면 자동으로 의존 주입이 되고, 빈으로 등록된 클래스에 @Transactional 어노테이션을 사용하면 트랜잭션이 적용되고, @Controller 어노테이션을 사용한 클래스가 서블릿으로 사용되고, @PostMapping, @GetMapping 어노테이션들이 기능으로써 동작한다.

 

 단순 어노테이션이 이렇게 어떤 기능으로써 동작할 수 있는 가장 첫번째 이유는 해당 클래스가 '빈'이기 때문이다. 내부적으로 IoC 컨테이너에서 관리되는 빈 리스트를 읽은 후 리플렉션과 프록시 등을 통해 이러한 기능들을 부여하기 때문이다. 

 

# 돌아갈 수 있다면...

 싱글톤으로 관리되며, 스프링에서 제공하는 기능을 사용할 클래스를 빈으로 등록하는 편입니다. 이러한 빈은 스프링에서 제공하는 여러 기능들을 런타임 시 부여할 수 있다는 점에서 확장성을 향상시킨다고 생각합니다.

 

# SHA256 을 유틸성 클래스로 설계한 이유 및 회고

public class SHA256 {
    public static String encrypt(String plainText){
        try{
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(plainText.getBytes());
            return bytesToHex(md.digest());
        }catch(Exception e){
            log.error(e.getMessage());
            throw new EncryptException(ErrorMessage.SHA256_ENCRYPT_FAIL);
        }
    }

    private static String bytesToHex(byte[] bytes){
        StringBuilder builder = new StringBuilder();
        for(byte b : bytes){
            builder.append(String.format("%02x", b));
        }
        return builder.toString();
    }
}

 위와 같이 SHA256 클래스를 static 메서드로 이루어진 유틸성 클래스로 설계한 이유는 다음과 같았다.

 

1. 스프링에서 제공하는 기능을 사용하지 않음

2. 상태 값이 없음

3. 멀티 쓰레드 환경에서 공유해도 안전함

4. 클라이언트에서 이를 사용하기 위한 추가작업(멤버필드 추가, 생성자 메서드 수정)이 필요없음

 

 이렇게 놓고보니 결국 싱글톤으로 관리되는 빈을 사용하지 않고 static 메서드 클래스를 사용하는 이유는 딱 하나. 4번 뿐이었다. 결국 내가 static을 선택한 이유는 클라이언트 입장에서의 코드 편의성 하나였던 것이었다.

 

 static에 대한 개발자들의 여러 의견들을 통해 static 사용을 지양하는 여러 이유를 찾을 수 있었다. 메모리의 메서드영역에 저장되어 GC 불가 및 메모리 문제를 야기할 수 있고, 확장이 불가하고, 상태 값 역할로 공유해서 사용할 경우 추론이 어렵고, 공유로 인한 문제가 발생할 수 있고 등등... 그 중 나의 프로젝트 구조에서 크게 와닿았던 문제가 바로 테스트가 까다롭다는 점이었다.

 

 최초 테스트 코드 작성 시 SHA256 클래스를 모킹 후 스터빙하기 위해 시도했으나 실패했다. 확인결과 새로운 의존 라이브러리를 사용하여 테스트 메서드마다 클래스를 Mocking, 스태틱 메서드를 stubbing해야 했고, 테스트가 끝나면 클래스의 Mocking을 해제해야했다. 배보다 배꼽이 더커진 느낌이 들어 클래스를 목킹하지 않고 SHA256 호출 부분에서 문제가 발생하지 않도록 요청이나 응답 테스트 픽스처들을 SHA256 클래스의 실제 동작에 맞게 신경써서 생성하게 되었다. 

 

 그 결과, 요청 픽스처가 변경되면 응답 픽스처도 변경해야했고, SHA256 클래스 내에서 문제가 발생하면 테스트가 실패하는 구조가 되었다. 테스트 메서드와 테스트 픽스처가 SHA256 클래스 실제 구현부에 강하게 결합되버린 것이다. 결과적으로 단위 테스트가 아니라 단위 + SHA256 테스트가 되버린 것...이다.

 

 만약 SHA256을 빈으로 등록했다면 쉽게 모킹, 스터빙이 가능했을 것이고, 테스트 메서드는 SHA256의 구현에 신경쓸 필요가 없으므로 단위 테스트의 목적에 맞게 구현됐을 것이다. 클라이언트 코드의 가독성도 떨어지지 않는다. 멤버필드 하나만 추가해주면 되기 때문이다. 빈으로 등록하는 것이 더 좋은 선택지였지 않았을까 하는 아쉬움이 남는다

 


Q3. 동일 ID 값을 가진 요청 데이터로 회원가입 요청이 빠르게 두번 연속으로 발생할 경우 동일한 ID로 회원가입이 될 것 같나요?

 

이것도 질문의 의도를 이해하지 못했다. 이 질문을 듣는 순간 머릿속이 아주 새 하얗게 변한게 생각난다. 면접관님께서는 동일 ID를 로그인에 사용될 ID로 말하고 있었지만, 나는 엔티티의 키로 사용하는 ID로 이해해버렸다.

 쓰기 락을 걸거나 기본키로 ID를 사용한다고 말했다.

 

# 또...돌아갈 수 있다면...

네 유저 ID가 키 값이 아니기 때문에 동일한 userId로 요청이 빠르게 중복되어 들어올 경우 테이블에 데이터가 중복되어 적재될 것 같습니다. userId를 유니크 키로 하거나, Id 컬럼을 없애고 userId를 PK로 사용해도 될 것 같습니다!

 

# 실제 테스트 및 회고

  실제 테스트를 해봤다. 그런데 이미 userId를 기본키로 해놨었다. 정신머가리가 빠졌다는 것을 다시한번 느꼈다. 어쨌든 동시성 테스트를 curl를 통해 진행하였다. 그 결과 첫번째 요청은 정상 수행됐으나 두번째 요청은 기본키 제약조건 위반으로 인해 런타임 예외가 발생하였다. 해당 예외에 대한 예외처리를 하지 않았기 때문에 '시스템에서 알수없는 에러가 발생하였습니다.'라는 문구가 클라이언트에게 응답되었다. 해당 예외에 대한 예외처리를 통해 적절한 메시지를 내려주면 좋았을 것 같다.

 


Q4. Spring Security에 적용하신 JsonAuthorizationFilter는 굳이 필요하다고 생각들지 않은데 사용한 이유가 있나요? 제가 생각했을 때는 UserDetails랑 UserDetailsService 추가해주면 될것같은데요?

 

A4. 어… 저는 필요하다고 생각합니다. 어쨌든간에 UsernamePasswordFilter에서는 폼 형식으로 온 요청에 대해 getParameter 메서드로 ID와 PW를 추출하고 있기 때문입니다. 로그인에서 Json 형식으로 올 경우 해당 필터가 값을 추출하지 못합니다.. 주저리 주저리.. 그래서 JsonAuthorizationFilter 클래스가 충분히 가치(갑자기 뭔 가치야 ㅁㅊ놈아..)있는 클래스라고 생각합니다.

 

# 또... 돌아갈 수 있다면...

JsonAuthorizationFilter 는 꼭 필요합니다. 말씀하신대로 UserDetails와 UserDetailsService를 추가하여 구현하는 방법도 있지만, 그 방법은 스프링 시큐리티 필터를 통해 인증을 처리할 때 유효합니다.

 

 spring security 설정을 건드리지 않고 UserDetailsService를 구현하여 인증 처리를 수행한다는 것은 security의 기본 설정에 UserDetailsService를 커스텀하여 적용하는 경우입니다. security 기본 설정은 formLogin 방식이고, 이는 곧 UsernamePasswordAuthenticationFilter가 추가된다는 뜻입니다.

 

 이 필터는 폼 방식으로 들어온 요청만을 필터링할 수 있으므로 Json 형식으로 들어오는 요청을 받아야한다면 이를 대신할 수 있는 커스텀 필터가 꼭 필요합니다. 저는 이 커스텀 필터로 JsonAuthorizationFilter를 사용한 것이기에 JsonAuthorizationFilter는 꼭 필요한 클래스라고 생각합니다.

 


Q5-1. CloseableHttpClient를 사용한 이유가 있나요?

나) 네 Connection Pool로 관리되어 Connection 생성으로 인한 오버헤드를 아낄... 주저리주저리…

면접관님) 흠.. 이거 말고 다른거 사용해보셨나요? restTemplate 같은거요. CloseableHttpClient는 요즘 잘 사용안하는것 같은데

나) 네 사용해봤습니다. (여기서부터 해탈했다. 질문에 대한 답변을 제대로 한게 없으니 내가 아는 지식에 대해 자신감을 갖지 못했기 때문이다.) 사실  이걸 사용한 이유는 실무 프로젝트 중 HttpClient 사용 시 주저리 주저리… 그때 이 CloseableHttpClient를 통해 잘 해결한 경험이 있기 때문에 활용하면 좋겠다 라고 판단했던것 같습니다. (왜 이딴식으로 대답했는지 모르겠다. 정말... CloseableHttpClient로 포스팅도 하고, 현업에서도 겪은 문제라 잘 아는데말이다... 이 상황을 벗어나고 싶었던걸가 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ...훌쩎)


# 또...또 돌아갈 수 있다면...

CloseableHttpClient는 커넥션 풀에서 커넥션을 관리할 수 있습니다. RestTemplate을 사용할 경우 HTTP 통신에 필요한 커넥션을 생성하고, 통신이 끝나면 해당 커넥션을 삭제하는 걸로 알고 있습니다. 하지만 CloseableHttpClient는 커넥션 풀에서 관리되는 커넥션에 대한 설정도 할 수 있는데 이를 통해 통신이 끝난 커넥션을 일정 시간동안 대기시켜 놓을 수 있습니다. 즉, 재사용이 가능하며 이는 곧 커넥션을 맺는 오버헤드를 감소시킬 수 있습니다.

 정리하면 커넥션 풀 및 커넥션 설정을 관리할 수 있고, 요청이 몰릴 경우 커넥션 생성으로 인한 오버헤드를 줄일 수 있어 성능상의 이점을 가져올 수 있기에 CloseableHttpClient을 사용하였습니다.

 


Q6. PrintWriter를 굳이 선택한 이유가 있나. (혼잣말이셨음)

 

이건 혼잣말로 말씀하신건데 생각해보지 않은 내용이었다. PrintWriter에 대해 찾아보고 도루마무를 해봤다.

 

# 진짜 마지막으로 돌아갈 수 있다면...

 

(면접관님의 혼잣말이 끝나시는 순간 자신감있게 일어서며) PrintWriter는 문자 텍스트를 응답할 때 사용하는데 accessToken은 문자 텍스트 형태이기 때문에 이를 사용했습니다. 하지만 범용성을 생각한다면 OutputStream을 사용하는 것이 더 좋은 선택지라고 생각이 드네요! 감사합니다!

 

...

...

...

...

...

 

 

 

 

자신의 생각을 말해야하는 자리이니만큼 컨디션 관리는 매우 중요했지만, 컨디션 관리를 너무 못했다. 너무 긴장한 탓인지 면접관님들의 질문에 큰 부담을 갖고 임한것도 문제였다. 면접관님들은 그냥 궁금해서 물어본건데 나는 모든 질문에 '이 코드에 뭔가 문제가 있어서 물어보는구나' 라는 생각을 깔고 임했기 때문이다. 그래서 질문을 받을수록 내가 알고 있는 것들이 잘못된걸까라는 의심을 하게 되고, 내 생각을 적극적으로 전달하지 못하게 되었다. 이 부분이 정말 후회되는 부분이다.

 

 누군가 면접은 경험이라고 했다. 면접이 끝나는 순간 이 말이 크게 와닿았다. 하지만 결코 후회뿐인 면접은 아니었다. 면접을 통해 내 생각을 다시 한번 정리할 수 있고, 평소라면 생각할 수 없었던 물음들에 대해 고민하게 된 좋은 경험이었다.

 너무 가고싶었던 기업인만큼 아쉬움이 크지만, 다음 면접에서는 이런 후회를 다시 겪지 않도록 자신감 있게 임할것이다. 화이팅!! 

반응형

+ Recent posts