어플리케이션은 완벽할 수 없다. 다만 완벽을 지향할 뿐.

Joshua_Kim·2024년 7월 14일
2
post-custom-banner

🌱 0. 들어가며

⏰ 시간아 조금만 느리게 가줘

시간이 참 빠르다.
날짜를 확인할 때 7월이라는 것도 낯설었는데 벌써 7월 중순이다.
항해도 시작한지 얼마 되지 않은 것 같은데 벌써 4주차를 마무리하고 5주차를 향해 가고 있다.

이번 주차 회고를 쓰기 전 지금까지 쓴 회고 글을 읽어봤다.
항해 여정을 통해 내가 무엇을 배웠고, 어떤걸 했는지 돌이켜보면 짧은 시간이지만 꽤나 성장한 것 처럼 느껴진다.

특히, 백엔드 엔지니어로서 요구사항을 분석하고 각각의 도메인의 관점에서 어떤 책임을 가지고 있는지 분석하고 고민하고 이해하는 경험을 했다고 생각한다. 그리고 어떤 방향성을 가지고 고민해야하는지를 조금씩 느껴나가는 것 같다.
새삼, 조영호님의 객체지향의 사실과 오해 에서 읽었던 내용들이 단순한 텍스트를 넘어 '아 이게 이런 느낌인건가?' 라는 생각도 들엇다.
이렇게 성장하는 거겠지..? 😋

항해가 아니었다면, 어떤 방향성을 가지고 백엔드 엔지니어로서의 역량을 기르고 공부해야할지 갈피를 잡지 못했을 것 같다.
진짜 너무 빡세고 가끔 포기해 버리고 싶기도 하지만, 매주 이렇게 회고를 할 때마다 느끼는 거지만 항해를 하기 정말 잘했다고 생각한다.

🔧 이번주에 주어진 과제들..

이번주는 간단히 말하면, 저번주에 설계했던 '콘서트 예약 시스템' 의 서버를 실제로 구축하는 것이었다.
STEP 7 은 Swagger 를 붙이는 것이라서 간단했지만, 실체는 STEP 8 였다.
비지니스 Usecase 를 개발하고 테스트를 작성하는 것.

기능 개발의 완료 라는 말이 이번주 내내 나에게 부담이 되어 눌렀다. 🫠


🍒 1. 4주차 항해 여정 회고

가장 빡셌던 이번 주..

7월 12일 금요일 새벽 3시 10분.
마지막 과제 PR 을 올리고 바로 침대에 쓰러졌다.

이번주는 지금까지 항해 여정 중에서 가장 빡셌었다.
나만 그렇게 느낀 것이 아니라 항해를 함께 하는 대부분의 항해러들의 감상이 그랬다.

- 윤XX 님의 이번주 감상....🫢

저번 주에 몸이 너무 안좋아져서 최대한 무리를 하지 않으려고 했지만
과제 제출 마지막 날인 목요일은 다음날 새벽3시까지 겨우 해서야 과제를 제출 할 수 있었다.

과제를 하며 챌린지 되었던 지점

1. Token 을 발급하고 그 Token 을 어디서 어떻게 검증할 것인가?

💡 고안해본 부분

  • Custom Annotation 을 만든다.
  • Interceptor 를 통해 Token 을 검증하도록 한다.
  • Resolver 를 통해 검증된 Token 을 받아와서 비지니스 레이어에 전달하도록 한다.
@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")
            if (token == null) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "QUEUE-TOKEN is missing")
                return false
            }

            if (!isValidToken(token)) {
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid QUEUE-TOKEN")
                return false
            }

            request.setAttribute("validatedToken", token)
        }
        return true
    }
    
@Component
class ValidatedTokenResolver : HandlerMethodArgumentResolver {
    override fun supportsParameter(parameter: MethodParameter): Boolean = parameter.hasParameterAnnotation(ValidatedToken::class.java)

    override fun resolveArgument(
        parameter: MethodParameter,
        mavContainer: ModelAndViewContainer?,
        webRequest: NativeWebRequest,
        binderFactory: WebDataBinderFactory?,
    ): Any? = webRequest.getAttribute("validatedToken", RequestAttributes.SCOPE_REQUEST)
}

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class TokenRequired

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class ValidatedToken

@TokenRequired 는 토큰이 헤더에 있는지 확인하고, 유효한지 검증하도록 한다.
@ValidatedToken 은 검증된 토큰을 꺼내와서 비지니스 레이어에 전달하도록 한다.

커스텀 어노테이션 실제 사용 예시
	/**
    * 콘서트 예약 가능 날짜 목록을 조회한다.
    */
   @TokenRequired
   @GetMapping("/concerts/{concertId}/schedules")
   fun getConcertSchedules(
       @ValidatedToken token: String,
       @PathVariable concertId: Long,
   ): ConcertResponse.Schedule =
       ConcertResponse.Schedule.from(
           concertService.getConcertSchedules(
               token = token,
               concertId = concertId,
           ),
       )
  • @TokenRequired 가 붙어 있으므로, 만료되지 않고 유효한 토큰이 헤더에 있는지 확인한다.
  • @ValidatedToken 을 통해 꺼내온 token 값을 서비스레이어에 전달한다.
    - 서비스레이어에서 해당 token 으로 대기열을 찾고, 그 대기열을 검증한다.

2. 대기열의 상태를 어떻게 관리할 것인가?

대기열의 상태를 어떻게 관리할지 고민을 하다가 스케쥴러에게 지속적인 검증과 상태 변경을 위임했다.

/**
  * 스케쥴러를 통해 queue 의 Proccessing 상태인 상태의 queue 개수를 유지시킨다.
*/
@Transactional
fun maintainProcessingCount() {
    val neededToUpdateCount = ALLOWED_MAX_SIZE - queueManager.countByQueueStatus(QueueStatus.PROCESSING)
    if (neededToUpdateCount > 0) {
        queueManager.updateStatus(
            queueIds = queueManager.getNeededUpdateToProcessingIdsFromWaiting(neededToUpdateCount),
            queueStatus = QueueStatus.PROCESSING,
        )
    }
}
// 스케쥴러
@Component
class QueueScheduler(
    private val queueService: QueueService,
) {
    @Scheduled(fixedRate = 60000)
    fun maintainProcessingCount() {
        queueService.maintainProcessingCount()
    }
}
  • Proccessing 상태인 대기열의 개수를 1분마다 확인해서 그 수를 유지시킨다.

3. 예약과 좌석의 상태를 어떻게 관리할 것인가?

    /**
     * 결제를 진행한다.
     * 1. reservation 의 user 와, payment 를 요청하는 user 가 일치하는지 검증
     * 2. payment 수행하고 paymentHistory 에 저장
     * 3. reservation 상태 변경
     * 4. 토큰의 상태 변경 -> completed
     */
    @Transactional
    fun executePayment(
        token: String,
        userId: Long,
        reservationIds: List<Long>,
    ): List<PaymentServiceDto.Result> {
        val user = userManager.findById(userId)
        val requestReservations = reservationManager.findAllById(reservationIds)

        // 결제 요청을 시도하는 user 와 예악한 목록의 user 가 일치하는지 확인한다.
        if (requestReservations.any { it.user.id != userId }) {
            throw PaymentException.InvalidRequest()
        }

        // 결제를 한다.
        val executedPayments =
            paymentManager.execute(
                user,
                requestReservations,
            )

        // 결제 내역을 저장한다.
        paymentManager.saveHistory(user, executedPayments)

        // reservation 상태를 PAYMENT_COMPLETED 로 변경한다.
        reservationManager.complete(requestReservations)

        // queue 상태를 COMPLETED 로 변경한다.
        val queue = queueManager.findByToken(token)
        queueManager.updateStatus(queue, QueueStatus.COMPLETED)

        // 결과를 반환한다.
        return executedPayments.map {
            PaymentServiceDto.Result(
                paymentId = it.id,
                amount = it.amount,
                paymentStatus = it.paymentStatus,
            )
        }
    }
  • 위의 플로우로 결제를 진행했다.
  • 결제가 성공적으로 진행되면, 대기열의 상태를 Completed 로 변환 시키고, 스케쥴러를 통해 Processing 상태의 대기열의 수를 유지시키도록 한다.

💎4주차 과제도 PASS !

이번주까지 모든 과제를 통과했다.
개인적으로 이번 주 과제가 정말 빡셌고 할 일들이 많았기 때문에 통과했다는 것에 더 큰 쾌감이 있었다.


🍎 2. 어플리케이션은 완벽할 수 없다. 다만 완벽을 지향할 뿐.

실제 로직을 구현하면서 깨달은 것들

이번주는 저번주 설계한 내용을 바탕으로 실제 비지니스 로직을 구현을 했다.
처음 로직을 구현하고 나서 주어진 조건내에서 완벽하게 구현했다고 생각했다.
그렇게 밤늦게 코딩을하고 자고 일어나면 다시 그 로직의 빈틈이 보이고, 실패 테스트를 작성해보니 완벽하지 않다는 것을 발견했다.
이런 플로우가 각각의 기능개발을 진행하면서 동일하게 몇번이고 반복되었다.

특히, 이번 과제의 '대기열' 관련 로직이 그랬다.
처음 생각에는 구현을 하면서 이 정도면 요구사항대로 잘 구현한 것 같았는데, 다음날 또 관점과 생각이 달라지고 빈틈이 보여서 여러번 수정을 거듭했다.

이러한 과정을 반복하면서 깨달은 점은 '어플리케이션은 완벽할 수 없다.' 는 것이었다.
로직은 완벽할 수 없다.
혹여 그 당시 어플리케이션이 완벽하더라도, 서비스가 커져가고 발전해나가는 이상 변경은 불가피하다.

백엔드 개발자로서 중요한 역량중 하나는 요구사항을 잘 분석하는 것이었다.
이것이 저번주 설계를 해보면서 느끼고 깨달은 것이었다.
그렇다면, 그 요구사항의 분석에 기반해서 만든 로직을 서비스의 성장과 변화에 따라 주기적으로 관리하고 변경을 하는 것도 큰 역량이라고 생각이 들었다.
그리고 그러기 위해서는 변경에 유연하도록 확장성있게 처음 설계를 잘 하는 것이 중요하다고 생각이 들었다.

끊임없이 내가 짠 코드와 어플리케이션에 책임을 가지고 고민해야한다.
그리고 그러기 위해서는 내 코드를 누가 읽어도 이해하기 쉽고 합리적으로 만들어야한다.
그렇기 때문에 더더욱 좋은 설계, 좋은 아키텍쳐, 께끗한 코드에 대해 강조하고 중요하다고 하는 것이구나 라고 생각했다.

개발자가 만든 어플리케이션은 결코 완벽할 수 없다.
만들어 놓은 그대로 영원할 수 없다.
서비스는 진화하고, 요구사항은 변경된다.

만들 때 완벽을 지향하면서 만들지만, 혹여 완벽하다고 생각하는 오만함으로 코드의 변경을 두려워하거나 거부해서는 안된다.
'내 코드를 사랑하지 말라'는 말이 그래서 나왔구나 라고도 생각이 들었다.


🍭 3. 토요지식회 - 항해에서 내 이야기를 나누다.

이번 항해 정기 모임에서 내 이야기를 나누게 되었다.
개인적으로 개발자 커뮤니티에서 인사이트를 나누게되어 참 기쁘고 영광스러웠다.
예전에 동문 개발자 모임에서 발표했던 '법대생이었던 내가 일어나보니 개발자가 된 건에 대하여' 라는 주제로 발표를 했다.

법대생 출신에 30대에 개발자가 된 내 삽질 인생 이야기..
발표는 언제나 떨리지만, 내 이야기를 개발자들에게 나누고, 그들에게 조금이라도 인사이트를 줄 수 있다는 것이 기뻤다.

🙏🏻 4. 글을 마치며

이번 한 주는 정말 뿌듯했다.
지금까지 완수했던 과제중에 가장 힘들었고 내용이 많았었고, 그것들을 모두 해내었다.
그리고 항해에서 내 이야기까지 나눌 수 있었던 이번 주.

다음주는 드디어 Chapter2 가 마무리 된다.
이번주의 과제 주제인 Logging, Exception Handling 까지 잘 마무리해서 훌륭한 서버를 구축하고 싶다.

이번 한 주도 화이팅!!

지난 회고 보러가기

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

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

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

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

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

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

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

profile
인문학 하는 개발자 💻
post-custom-banner

0개의 댓글