항해의 중간지점, 나는 얼마나 성장했나.

Joshua_Kim·2024년 7월 21일
8
post-thumbnail

🌱 0. 들어가며

❤️‍🔥 Chapter 2 가 끝났다.

마침내, 항해 플러스 백엔드 5기 과정의 Chapter2 가 끝났다. 👏🏻

Chapter 2가 끝났다는 것은, 항해를 하는 동안 만들고 완성해 나갈 서비스의 큰 뼈대가 완성되었다는 말이다.
내가 선택했던 서비스의 시나리오는 '콘서트 대기열 시스템' 이었고,
이 서비스의 요구사항에 맞춰 Usecase 를 설계하고 개발하는 것이 이번 챕터의 목적이었다.

기간은 총 3주였고, 그 기간내에 과제를 완수하는 것은..
게다가 직장을 다니면서 모든 과제를 다 해내는 것은....

넘나, 넘나, 넘나리.. 힘든일이었다..🥹

  • 지난 3주간의 여정을 담은 PR들..
  • 지금 회고를 쓰면서 다시 들어가서 보는데 뭔지 모를 뿌듯함이 몰려온다.

💎 Chapter 3 에서 내가 공부 할 것

Chapter2 를 통해 서비스의 요구사항을 개발하고 서버를 구축했다면,
Chapter3 에서는 이 어플리케이션을 실제로 서비스를 할 때 발생하는 여러 문제들을 대응하는 방법을 다룬다.

대표적인 이슈로는 '대용량 트래픽''동시성 이슈' 가 있다.
내가 항해플러스 백엔드 과정을 시작하기로 결심했던 큰 이유중 하나가 바로 이것에 대한 내용이었다.
이 두가지 이슈는 백엔드 개발자가 회사에서 서비스를 개발하고, 개선해나가면서 무조건 마주하게 되는 문제다.
그리고 이 두가지 이슈를 서버에서 제대로 처리하지 못한다면 예상치 못한 큰 장애와 손실을 가져다 줄 수 있다.
그렇기에, 백엔드 엔지니어는 이 부분에 대해 단단하고 견고한 훈련이 되어있어야한다.

하지만, 내게 저 부분에 있어서 어떻게 구현하는지, 어떻게 대응하는지, 어떻게 해결하는지 물어본다면..
자신있게 대답할 수 있을까..?
글쎄.. 🤔

이제 벡엔드 개발자로 만 3년차가 되었지만, 부끄럽게도 자신있게 대답을 하지 못했다.
그렇기에 더더욱 이번 Chapter를 통해 이 부분에 있어서 단단하고 자신있는 기술적 탁월함을 성취하고 싶다.
RedisKafka 를 사용한 대용량 트래픽과 동시성 제어에 대한 학습에 큰 기대를 가지고 있다.


🍇 1. 5주차 항해 회고

🤮 힘들었다. 하지만 해냈다.

상대적으로 4주차에 비해서는 과제의 양이 많지 않았다.

Step9. 구현해야할 Filter 와 Interceptor

내 어플리케이션에서 구현한 Filter 와 Interceptor 는 로깅을 위한 Filter 와 토큰 검증을 위한 Interceptor 였다.

  • LoggingFilter
@Component
class LoggingFilter : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
        val requestWrapper = ContentCachingRequestWrapper(request)
        val responseWrapper = ContentCachingResponseWrapper(response)
        logger.info(getRequestLog(requestWrapper))
        filterChain.doFilter(request, responseWrapper)
        logger.info(getResponseLog(responseWrapper))
    }
    private fun getRequestLog(request: ContentCachingRequestWrapper): String {
        val requestBody = String(request.contentAsByteArray)
        return """
            |=== REQUEST ===
            |Method: ${request.method}
            |URL: ${request.requestURL}
            |Headers: ${getHeadersAsString(request)}
            |Body: $requestBody
            |================
            """.trimMargin()
    }
    private fun getResponseLog(response: ContentCachingResponseWrapper): String {
        val responseBody = String(response.contentAsByteArray)
        return """
            |=== RESPONSE ===
            |Status: ${response.status}
            |Headers: ${getHeadersAsString(response)}
            |Body: $responseBody
            |=================
            """.trimMargin()
    }
    private fun getHeadersAsString(request: HttpServletRequest): String =
        request.headerNames.toList().joinToString(", ") {
            "$it: ${request.getHeader(it)}"
        }
    private fun getHeadersAsString(response: HttpServletResponse): String =
        response.headerNames.joinToString(", ") {
            "$it: ${response.getHeader(it)}"
        }
}
  • 처음에는 Interceptor 로 구현하려고 했었지만, 멘토링을 받은 후에 Filter 로 구현하는 것으로 변경했다.
  • Request 와 Response 에 대한 로깅이므로, Servlet 컨테이너에 의해 관리되는 Filter 가 더 적합하다고 판단했다.
  • 더군다나, Request 와 Response 는 Spring 과는 무관한 녀석이므로, Spring Context 외부에서 동작하는 Filter 에서 처리하는 것이 맞다고 판단했다.
  • TokenInterceptor
@Component
class TokenInterceptor(
    private val jwtUtil: JwtUtil,
) : HandlerInterceptor {
    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
    ): Boolean {
        if (handler is HandlerMethod) {
            val requireToken =
                handler.hasMethodAnnotation(TokenRequired::class.java) ||
                    handler.beanType.isAnnotationPresent(TokenRequired::class.java)
            if (!requireToken) {
                return true
            }
            val token = request.getHeader(QUEUE_TOKEN_HEADER)
            if (token == null) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "$QUEUE_TOKEN_HEADER is missing")
                return false
            }
            if (!isValidToken(token)) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid $QUEUE_TOKEN_HEADER")
                return false
            }
            request.setAttribute(VALIDATED_TOKEN, token)
        }
        return true
    }
    private fun isValidToken(token: String): Boolean = jwtUtil.validateToken(token)
}
  • 이 컴포넌트에서는 발급한 Jwt 토큰의 유효성을 검토한다.
  • @TokenRequired 라는 어노테이션이 붙어있는 메서드에서 동작하도록 설계했다.

Step10. 정상적으로 구동되는 서버 완성 및 통합 테스트 작성

  • 126개의 단위 테스트 및 통합테스트를 작성했고, 모두 성공시켰다.

Step10.5 Github Actions 를 활용한 Docker push CI 작성

  • Github Actions Workflow 를 통해 Main 브랜치에 push 되면 Docker 파일이 빌드되고 컨테이너 이미지가 github container registry 에 저장되도록 했다.

🏅 지금까지 All Pass !

  • Chapter2가 마무리 되는 현재까지 과제를 모두 통과했다.
  • 더군다나 이번 과제에서는 따봉👍🏻 을 받았다. 기분이 매우좋으다 크크..

🍐 2. 딱 절반이 지난 지금, 나는 얼마나 성장했나.

👨🏻‍💻 코딩을 할 때 더 시간이 소요된다.

많은 고민 없이 하던 방식대로 손이 먼저 나가고 어느새 하나의 메서드를 완성하고 또 다른 작업을 진행했었다.
하지만, 항해를 시작하고 코딩을 할 때 멈칫 멈칫 할 때가 많다.

"이렇게 짜면 테스트하기 어려울 수 있을 것 같은데..?"
"패키지 구조는 이게 맞나..?"
"이 메서드는 이 클래스의 책임이 아닌 것 같은데 어떻게 분리시키지?"
"이 도메인은 여기까지 관여를 하면 안될 것 같은데... 설계를 다시 고민해 봐야하나?"

고민 없는 코드는 좋은 코드가 될 수 없다.
단순히 기능 구현의 고민이 아닌, 코드의 품질을 위한 고민을 하기 시작했다.
내 코드가 나만 알아보고 돌아만 가서 끝나는 악취나는 녀석이 되지 않게끔 내 최선을 다해 고민하려는 스탠스를 가지게 되었다.

그리고, 경험치가 쌓이면 이 시간도 단축되고 당연하듯 코드를 쓰게 되겠지..🤗


🐦 객체가 서비스에서 숨쉬며 살아있다는 말이 조금은 이해가 되기 시작했다.

저번 주 회고에서도 언급했지만, 조영호님의 객체지향의 사실과 오해에서 읽었던 대목들이 눈에 들어오기 시작했다.

단순히 @Service 어노테이션 붙이고, 클래스명에 xxxxService 로 만들고, 로직을 작성하고...
나는 그냥 클래스들을 딱딱하고 기계적인 무언가, 정적인 무언가로 생각하며 코딩을 했던 것 같다.

객체들에게 책임을 부여하니까, 이 녀석들이 그 책임을 가지고 일을 하더라.
그리고 그 책임을 가진 녀석들에게 적당한 이름을 붙여주니까 코드가 더 보기 좋아지더라.

실제로 회사에서 코드를 그렇게 변경해봤다.
xxxIssuer, xxxMaker, xxxManager
@Component 를 붙이고, 각자의 책임을 부여하고 xxxService 에서 조립을 하니까 훨씬 코드가 명확해졌다.
그리고, 무엇보다 내 어플리케이션에서 작은 친구들이 각자 부여된 하나의 역할을 충실하게 수행하는 것을 보니 기분이 좋았다.

🧪 테스트코드가 조금은 익숙해졌다.

아, 정말 개인적으로 큰 성장이라고 생각한다.
테스트코드 한 줄도 짜지 못하고, JUnit 을 듣기만 해봤던 내가 테스트코드가 익숙해졌다고 글을 쓰고 있다니..
아직은 그래도 낯선 부분도 있고, 더 좋은 테스트코드를 짜기 위한 고민과 노력이 필요하겠지만, 항해의 반이 지난 지금, 테스트코드가 조금은 익숙해졌다.


3. 글을 마치며

이제 반이 지났다.
언제 이렇게 시간이 갔나 싶다.
매주 회고글을 쓸 때마다 느끼는건데, 항상 이말을 쓰는 것 같다. '시간 참 빠르다.'

이제 시작하는 Chapter3 도 늘 그랬듯, 성실하게 최선을 다해 임해서 내가 성취하고 싶었던 배움의 기쁨을 누리고 싶다.

이번 주도 화이팅 💪🏻

지난 회고 보러가기

1주차 회고 - 테스트코드를 모르던 내게 찾아온 TDD
2주차 회고 - 코딩에 정답을 찾지말자. 고민을 통해 더 나아짐을 시작하자.
3주차 회고 - 좋은 코드를 위해서는 좋은 설계가 우선되어야 한다.
4주차 회고 - 어플리케이션은 완벽할 수 없다. 다만 완벽을 지향할 뿐.

항해에 관심이 있으시다구요?

항해플러스에서 벌써 백엔드 6기 모집이 시작된다고해요. (내가 벌써 선배..?)
제 회고글을 모두 읽어 보신 분들은 잘 아시겠지만, 이 과정을 통해 정말 많은 것을 누리고, 배우고, 경험하고, 느끼고 있습니다.

솔직히 말씀드리면, 이 과정은 마냥 즐겁지는 않아요.
고통스럽고, 힘들고, 많이 지칩니다. 😔

더군다나 직장을 다니면서 병행한다면 잠을 포기하고 시간을 많이 갈아 넣어야해요.
하지만, 지금 열심히 항해중인 제가 감히 자신있게 말씀드리자면, 이 과정을 통해 지금까지 경험하지 못했던 압축된 성장을 경험할 수 있습니다.

혹시, 관심이 있으시다면 지원하실 때 추천인 코드(HHPGS0893)를 작성해주신다면 할인이 된다고 해요 ㅎㅎ
고민되시는 분은, 댓글로 달아주시면 커피챗을 통해 이야기 해도 좋을 것 같습니다.

성장을 위해 시간을 쏟을 준비가 되신 주니어 분들에게 정말 진심을 다해 추천합니다.

profile
인문학 하는 개발자 💻

2개의 댓글

comment-user-thumbnail
2024년 7월 21일

더 많이 고민하는 개발자가 되었다는 건 분명한 성장의 증거..!! 응원합니다

답글 달기
comment-user-thumbnail
2024년 7월 22일

항상 5기의 좋은 귀감이십니다~남은 주차도 화이팅입니다!

답글 달기