딜리버리 프로덕트 개발팀의 개발 문화 - 주니어 디버깅 스터디
주니어 개발자의 성장을 목표로 진행한 에러 디버깅 스터디 과정을 소개합니다.
- 들어가며
- 스터디 구성 (Overview)
- [이론 스터디] 1. 에러 알림 대응 프로세스
- [이론 스터디] 2. ‘디버깅 마인드셋: 디버깅의 고통을 절반으로 줄여주는 고수들의 행동패턴 따라하기' 영상 시청
- 실전 디버깅 스터디 - 사례 공유
- 스터디를 통해 배운 디버깅 팁 소개
- 스터디 이후, 어떻게 달라졌을까?
- 마지막으로, 스터디 후기를 들려주세요.

들어가며
안녕하세요. 딜리버리 프로덕트 개발팀에서 일하고 있는 한경훈입니다.
개발자라면 프로덕션에 배포 후 예상치 못한 에러가 발생하거나, 한밤중에 긴급한 장애 알람을 받는 상황을 종종 겪습니다. 이러한 에러는 때로는 서비스의 안정성과 직결되는 중요한 문제가 되기도 합니다.
이러한 상황에서 중요한 것은 에러의 현상을 단순히 파악하는 데 그치지 않고, 시급도를 판단하고 근본적인 원인을 찾아 해결하는 것입니다. 문제를 깊이 있게 분석하고 해결하는 과정은 단순히 버그를 고치는 것을 넘어, 개발자로서의 실력을 쌓는 중요한 기회가 됩니다.
다양한 에러 상황에 체계적으로 대처하는 능력은 주니어 개발자가 성장하기 위해 반드시 갖추어야 할 핵심 역량이기도 합니다. 이에 저희 팀은 "주니어 개발자를 위한 에러 디버깅 스터디"를 기획하게 되었고, 약 10주간 스터디를 진행하였습니다. 이번 글에서는 스터디의 진행 과정과 그 과정에서 배운 값진 경험들을 공유하고자 합니다.
스터디 구성 (Overview)
스터디는 총 10주간 “이론 스터디”와 “실전 디버깅 스터디” 두 파트로 나누어 진행되었습니다.
이론 스터디(2주)
- 에러 알림 대응 프로세스: 모니터링 시스템에서 에러 알림이 울렸을 때, 상황을 파악하고 대응하는 워크플로우 교육을 진행하였습니다.
- 때마침 인프콘에서 디버깅을 주제로한 발표가 있었고, 실제 고수들의 행동패턴을 함께 학습했습니다.(럭키비키!)
실전 디버깅(8주)
-
실제 문제를 디버깅해보는 단계로, 매주 1시간씩 다음과 같은 타임 테이블로 진행되었습니다.
시간 내용 5분 • 주제 선정: 실제 발생했던 미처리 에러 중 하나를 선택하고 상황 설명 15분 • 각자 분석: 개별적으로 디버깅 방법 구상 30분 • 토론: 10분 단위로 각자의 해결 방안 공유 및 토론 10분 • 회고: 학습 내용 정리 및 느낀 점 공유
[이론 스터디] 1. 에러 알림 대응 프로세스

이론 스터디의 첫 번째 세션에서는 에러 알림 발생 시 효과적인 대응 방법에 대해 두 가지 내용을 중점적으로 다루었습니다.
1. 에러 알림 대응 프로세스
현상 분석: 무엇이 발생했는가?
에러를 마주쳤을 때 가장 먼저 해야 할 일은 현상을 정확히 파악하는 것입니다. 이는 다음 단계의 모든 의사결정의 기초가 됩니다. 현상 분석을 통해 장애 상황인지, 빠른 조치가 필요한 이슈인지, 혹은 마이너한 이슈인지와 같은 시급도와 영향범위를 파악할 수 있습니다.
원인 분석: 왜 발생했는가?
현상을 정확히 파악했다면, 그 다음은 원인을 추적해야 합니다. 원인 분석이란 에러를 발생시킨 근본적인 이유를 찾아가는 과정입니다. 시스템 로그, 리소스 등을 확인하며 가설을 세우고 사실로 확인된 점과 그렇지 않은 점을 구분하여 점점 범위를 좁혀나가는 과정입니다. 이는 마치 탐정이 범위를 좁혀가며 범인을 찾아가는 과정과 유사합니다.
적절한 해결책 정의 원인을 정확히 파악했다면, 그 근본 원인을 해결할 수 있는 적절한 해결책을 정의해야 합니다. 이 단계에서는 단순히 현상만 해결하는 임시 방편을 지양하고, 실제 문제의 근원을 제거하는 접근법을 선택해야 합니다. 비록 더 많은 시간과 노력이 필요할 수 있지만, 장기적인 시스템 안정성과 유지보수성을 위해서는 근본 원인을 해결하는 것이 필요합니다.
2. '현상'이 아닌 '원인'을 해결해야 하는 이유
현상 해결 | 원인 해결 |
---|---|
빠른 문제 해결 가능 | 해결에 시간이 더 소요될 수 있음 |
임시 방편적 성격이 강함 | 근본적인 문제 해결 가능 |
근본적 문제는 여전히 존재 | 근본적인 문제가 해결되어 장기적으로 더 안정적 |
원인을 해결하지 않을 경우 다음과 같은 문제가 발생할 수 있습니다.
문제의 재발생
근본 원인이 해결되지 않으면 동일한 에러가 반복적으로 발생할 수 있습니다. 이는 불필요한 수정 작업의 반복으로 이어져 개발 생산성을 저하시킵니다.
연쇄적인 새로운 문제 발생
표면적인 현상만 해결하다 보면 예상치 못한 부작용이 발생하여 새로운 문제가 나타날 수 있습니다.
디버깅 시간 증가
향후 유사한 문제 발생 시, 직전의 현상 패치 로직 때문에 실제 원인을 파악하기가 더욱 어려워질 수 있습니다. 이로 인해 문제 해결에 소요되는 시간과 노력이 더 증가할 수 있습니다.
[이론 스터디] 2. ‘디버깅 마인드셋: 디버깅의 고통을 절반으로 줄여주는 고수들의 행동패턴 따라하기' 영상 시청
스터디를 진행하는 시점에 감사하게도 인프콘에서 디버깅 마인드셋: 디버깅의 고통을 절반으로 줄이는 고수들의 행동패턴 따라하기 발표 영상이 공개되었습니다. 먼저 진행한 이론스터디가 워크플로우 중심이었다면, 이번 스터디에서는 고수들의 행동패턴을 통해 구체적인 디버깅 방법을 살펴볼 수 있었습니다. 도움이 되었던 몇 가지 내용을 소개합니다.

1. 디버깅 고수는 원인파악에 많은 시간과 노력을 투자한다.
디버깅 고수들은 문제 해결에 있어 원인 파악에 훨씬 더 많은 시간과 노력을 쏟습니다.
- 원인 파악(60%)
- 문제 해결(30%)
- 사후 처리(10%)
단순히 문제를 해결하는 데 급급하기보다, 문제의 근본적인 원인을 깊이 분석하고 이해하는 데 집중합니다. 반면, 숙련도가 낮은 개발자는 원인을 대충 파악하거나 바로 해결에 뛰어드는 경향이 있습니다. 고수들은 문제의 정상적인 동작을 명확히 정의하고, 최소 환경을 구축하여 관찰하며, 가설을 설정하고 검증하는 단계를 거칩니다.
2. 디버깅 고수가 되기 위한 5단계 가이드
- 문제 정의: 해결하려는 문제를 명확하게 정의합니다.
- 정상 동작 정의: 정상적인 환경에서 어떤 조건, 순서로 어떤 일이 벌어져야 하는지 정의합니다.
- 최소 제한 환경 구축 및 관찰: 문제가 발생하는 최소 환경을 구축하고 동작을 관찰합니다.
- 원인 탐색: 정상 환경과 문제 환경의 차이를 발생시키는 다양한 원인을 탐색합니다.
- 가설 설정 및 검증: 가장 그럴듯한 원인에 대한 가설을 설정하고 검증합니다.
3. 그 외 디버깅 고수들의 습관
- 작업 계획 수립: 작업 계획을 수립하면 시간 관리 및 집중력 유지에 도움이 되며, 문제를 다른 사람에게 설명하고 공유할 때 유용하게 활용할 수 있습니다.
- 적절한 디버깅 도구 활용: 상황에 맞는 디버깅 도구를 잘 사용하는 것이 중요합니다. 다양한 디버깅 도구를 학습해두고, 필요할 때 능숙하게 사용하는 것이 디버깅 속도를 높이는 데 큰 도움이 됩니다.
실전 디버깅 스터디 - 사례 공유
앞으로 실전 디버깅 스터디를 계획하실 분들께 도움을 드리고자, 저희가 진행한 스터디의 생생한 사례 하나를 각색하여 공유합니다.^^;
주제 선정 및 공유
- 최지수: 안녕하세요. 오늘 디버깅 스터디 주제로 “XX 시스템 로그인 과정 중 가끔 발생하는 NPE(Null Pointer Exception)” 문제를 함께 분석해보려 합니다. 각자 15분간 원인을 분석하고 공유하는 시간을 가지겠습니다.
java.lang.NullPointerException: Cannot invoke "java.time.LocalDateTime.format(java.time.format.DateTimeFormatter)" because "localDateTime" is null at com.kurly.toms.******.LocalDateTimeAdapter.*****(LocalDateTimeAdapter.java:16) at com.kurly.toms.******.LocalDateTimeAdapter.*****(LocalDateTimeAdapter.java:10) at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:73) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.write(ReflectiveTypeAdapterFactory.java:247) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:490) at com.google.gson.Gson.toJson(Gson.java:944) at com.google.gson.Gson.toJson(Gson.java:899) at com.google.gson.Gson.toJson(Gson.java:848) at com.google.gson.Gson.toJson(Gson.java:825) at com.kurly.toms.config.Gson2JsonRedisSerializer.serialize(Gson2JsonRedisSerializer.java:24) at org.springframework.data.redis.serializer.DefaultRedisElementWriter.write(DefaultRedisElementWriter.java:41) at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.write(RedisSerializationContext.java:292) at org.springframework.data.redis.cache.RedisCache.serializeCacheValue(RedisCache.java:372) at org.springframework.data.redis.cache.RedisCache.put(RedisCache.java:226) at org.springframework.cache.interceptor.AbstractCacheInvoker.doPut(AbstractCacheInvoker.java:87) at org.springframework.cache.interceptor.CacheAspectSupport$CachePutRequest.performCachePut(CacheAspectSupport.java:1031) at org.springframework.cache.interceptor.CacheAspectSupport$CachePutRequest.apply(CacheAspectSupport.java:1016) at org.springframework.cache.interceptor.CacheAspectSupport.evaluate(CacheAspectSupport.java:560) at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:433) at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:395) at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:74) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:220) at jdk.proxy3/jdk.proxy3.$Proxy243.save(Unknown Source) at com.kurly.toms.******.******.******.******(******.java:86)
(15분 후) 각자 디버깅한 내용 공유
- 최지수:
LocalDateTimeAdapter
로 데이터를 직렬화하는 과정에서 NPE가 발생하고 있어요. Token 객체의 시간 관련 필드 중 일부가 null값이었다는 것인데, 언제 null이 설정될 수 있는지 확인해보면 좋을 것 같아요. - 고산하: 저도 지수님과 마찬가지로 먼저 에러 발생 위치를 추적했어요. 그리고 최근 배포된 스프링부트 버전 업데이트와 연관성이 있을 것 같아, 해당 버전 업그레이드 이후 발생한 에러인지 확인하고 싶었습니다. 하지만 안타깝게도 과거 에러 로그가 이미 삭제되어 원인 분석이 어려웠어요.
- 김경록: 저도 에러가 발생한 로직부터 스택 트레이스를 따라가며 의심되는 로직들을 살펴봤어요. Repository save 시 PrePersist 어노테이션으로 등록일시 속성을 자동 설정하는데, 이 과정보다 Redis가 먼저 저장되는 특정 케이스는 없는지 의심되었어요.
(이어서) 각자 디버깅한 내용에 대한 피드백
- 한경훈: 에러 지점부터 시작해 문제를 좁혀 나가는 것 잘하셨습니다. 저는 먼저 단서를 더 수집하고 분석하는 것이 필요해보입니다. 예를 들면 어떤 API에서 어떤 상황에 발생한 에러인지, 에러가 발생한 객체는 어떻게 생겼는지 확인해보면 도움이 될 것 같습니다.
(이어서) 함께 디버깅하기
- 어떤 상황에서 발생한 에러인가?
- 고객사 로그인 API에서 Token 객체를 Redis에 저장하기 위해 직렬화하는 과정 중,
LocalDateTimeAdapter
에서 특정 날짜 필드를 직렬화할 때 널 포인터 예외(NPE)가 발생
- 고객사 로그인 API에서 Token 객체를 Redis에 저장하기 위해 직렬화하는 과정 중,
- Token 객체에 어떤 날짜 필드가 있는지? (문제를 유발한 후보 찾기)
- expirationTime, regDate, updDate
- Optional 속성은 없음
- Redis에 실제로 저장되는 데이터 형태 확인해보기 (실제 직렬화된 문자열 확인하기)
- 모든 날짜 필드가 직렬화되어 저장됨
{ "expirationTime":"2024-10-15T12:45:18", "regDate":"2024-10-14T12:45:18.203067358", "updDate":"2024-10-14T12:45:18.203067358" }
- 각 필드에 null이 들어갈 수 있는 상황에 대한 가설들을 먼저 모아보고 하나씩 점검해보는 것도 좋겠다.
- 에러로그를 보강하는 것도 좋겠다. 어떤 로그를 보완하면 다음 에러 발생시 추적이 용이할까?
- 에러 발생시 null값이 할당된 속성명을 정확히 알 수 있도록 로그 보강하기
- 모든 날짜 필드가 직렬화되어 저장됨
(50분 경과) 후기 공유
- 한경훈: 시간이 다 되었으니 오늘 스터디는 여기서 종료하겠습니다. 각자 스터디 후기를 간단히 말씀해주세요.
- 최지수: 이번 문제 해결 과정에서 트레이스뿐만 아니라, 연관된 다양한 단서들을 종합적으로 찾아나가는 접근법의 필요성을 배웠습니다.
- 고산하: 스프링부트 버전 업데이트와 연관 지어 문제를 좁은 시각으로 접근했던 것 같아요. 앞으로는 더 넓은 관점에서 문제를 바라봐야겠습니다.
- 김경록: 놓친 부분도 있지만 나름대로 문제의 상당 부분을 파악한 것 같아서 뿌듯합니다. 의심되는 부분을 집중적으로 분석하였는데, 나중에는 단서를 먼저 수집한 후 방향을 좁혀나가야겠습니다.
스터디를 통해 배운 디버깅 팁 소개
10주간의 스터디를 통해 배운 디버깅 팁 중, 각자 가장 인상 깊었던 3가지를 소개합니다.

1. 검증되지 않은 가설을 믿지 않기 (고산하, 김경록, 최지수)
에러 디버깅 과정에서는 여러 가설과 단서들이 제시됩니다. "최근 배포한 코드 때문일 것 같아요", "A서버 문제 같아요"와 같은 가설들도 디버깅의 유용한 시작점이 될 수 있습니다. 다만, 가설이 제시되면 반드시 관련된 로그나 데이터를 통해 해당 가설을 검증하는 과정을 거쳐야 합니다. 이 과정에서 에러 로그, 재현 여부, 모니터링 지표 등 확인된 사실들을 차근차근 모아가며 검증을 진행하는 것이 중요합니다.
2. 디버깅의 시작점은 단서를 수집하는 것 (최지수)
디버깅은 남겨진 단서를 수집하는 것에서 시작됩니다. 에러 발생 시 다음과 같은 단서들을 체계적으로 수집하면 문제 해결에 큰 도움이 됩니다.
- 에러 메시지와 스택 트레이스: 발생한 에러의 정확한 내용과 발생 위치를 파악할 수 있는 가장 기본적인 단서입니다.
- 로그 기록: 에러 발생 전후의 시스템 로그나 애플리케이션 로그를 수집하여 문제의 맥락을 파악합니다.
- 재현 조건: 어떤 상황에서 문제가 발생했는지 기록하고, 문제를 재현할 수 있는 조건이 있다면 함께 정리합니다.
3. 명확한 디버깅 목표 설정하기 (김경록)
디버깅을 시작할 때 가장 중요한 것은 "무엇을 확인하고 싶은가?"를 정의하는 것입니다. "API가 500 에러를 반환하는 원인 찾기", "특정 고객의 주문만 실패하는 원인을 찾아보자"와 같이 구체적인 목표를 세우면 필요한 정보를 효율적으로 찾을 수 있습니다. 반면 막연히 로그를 살펴보거나 코드를 보게되면 불필요한 확인 작업을 반복하게 되어, 더 많은 시간이 소모될 수 있습니다.
4. 에러가 발생한 API의 특성을 이해하기 (김경록)
에러가 발생한 API나 Topic의 특징을 파악하는 것이 중요합니다. 때로는 실제 문제가 아닌 False Alarm일 수 있고(의도된 예외 처리이나 에러로 알림이 울린 것), 시스템의 특성상 발생할 수밖에 없는 케이스일 수도 있습니다. 문제 해결에 앞서 해당 컴포넌트의 동작 방식을 이해하는 시간을 가져보세요.
5. 디버깅 과정을 쓰레드로 공유하기 (최지수)
디버깅 과정에서 검증 중인 가설이나 확인한 단서들을 쓰레드로 기록하는 것이 중요합니다. 쓰레드로 기록하면서 디버깅하면 생각을 정리하며 체계적으로 진행할 수 있어 더 나은 결과를 얻을 수 있습니다. 또한 진행 상황이 공유되어 팀원들에게 심리적 안정감을 줄 수 있고, 적절한 도움을 주고받을 수 있으며 중복 디버깅을 방지할 수 있습니다.
6. 문제 재현을 통해 사실 확인하기 (고산하)
문제를 실제로 재현하고 검증하는 과정도 디버깅에 있어 중요합니다. 로그나 모니터링 지표와 같은 단서만으로도 원인을 파악할 수 있는 경우가 있지만, 실제 재현을 통한 검증이 반드시 필요한 경우도 있습니다. 문제 상황을 직접 재현해보면 에러의 발생 조건을 정확히 파악할 수 있고, 문제의 범위와 영향도를 구체적으로 확인할 수 있으며, 해결 방향을 설정하는 데도 도움이 됩니다.
7. 중요한 문제 해결 후 회고하기 (고산하)
복잡하거나 파급력이 큰 문제 해결 이후에는 발생 원인과 해결 과정을 되돌아보는 시간을 갖을 필요가 있습니다. 회고를 통해 근본 원인을 더 깊이 이해하고, 추가로 필요한 개선 작업이 있는지 점검할 수 있습니다. 이때 문제 상황과 해결 과정을 문서화하여 팀 내 지식으로 공유하면, 향후 유사한 문제를 더 신속하고 효과적으로 해결하는 데 도움이 될 수 있습니다.
스터디 이후, 어떻게 달라졌을까?
디버깅 역량이 향상되어 에러를 더 세련되게 처리할 수 있게 되었습니다. 문제가 발생했을 때 침착하게 원인을 분석하고 해결책을 찾아내는 능력이 향상되었습니다. 주니어 개발자들이 성장하면 조직에도 긍정적인 영향을 미치는 것 같습니다. 주니어 개발자의 발전된 모습을 보며 다른 팀원들도 함께 성장할 수 있었고, 팀의 문제 해결 능력이 한 단계 업그레이드된 것을 느낄 수 있었습니다.
최지수



김경록

고산하

마지막으로, 스터디 후기를 들려주세요.
고산하
좋았던 점 이번 스터디에서 가장 알고 싶었던 것은 "다른 사람들이 디버깅 과정에서 어떤 생각을 하는지"였습니다. 스터디를 통해 이 부분을 이해하게 되었고, 각자의 접근 방식을 보면서 "이렇게 생각하고 접근할 수도 있구나"라는 새로운 관점을 얻을 수 있었습니다. 또한, 문제를 차근차근 분석하며 어디서부터 접근해야 할지 배우게 되었습니다. 이전에는 현상에 대한 즉각적인 해결 방법을 떠올리는 경향이 강했지만, 이번 스터디를 통해 문제의 현상과 원인을 파악하는 데 시간을 투자하는 것이 얼마나 중요한지 깨달았습니다.
아쉬웠던 점 스터디에서 함께 디버깅할 주제를 리스트업하는 것이 조금 어려웠습니다
김경록
좋았던 점 이전에 알지 못했던 새로운 디버깅 방법들을 배우는 계기가 되었습니다. 과거에는 문제가 발생하면 코드를 먼저 확인했었는데, 이제는 로그를 분석하며 현상을 파악하는 데 먼저 시간을 투자하게 되었습니다. 복잡한 문제일수록 현상을 충분히 파악하는 것이 문제를 더 빠르게 해결하는 데 효과적이라는 점을 체감하고 체득화할 수 있었습니다.
아쉬운 점 스터디 주제를 선정하는 과정이 쉽지 않았습니다. 또한, 난이도 있는 주제의 경우 디버깅에 충분한 시간을 할애할 수 없어서 깊이 있는 분석을 진행하지 못했던 점이 아쉬웠습니다.
최지수
좋았던 점 담당하던 레거시 시스템의 에러가 빈번하게 발생할 때 스터디를 진행해서 시너지가 있었습니다. 에러에 접근하는 방법을 구체적으로 배울 수 있었고, 하나의 에러를 각자의 방식으로 분석하고 논의하면서 더 깊이 있는 통찰을 얻었습니다. 또한, 에러 알림이 울렸을 때의 두려움도 없앨 수 있었습니다.
어려웠던 점 “분석에 실패하거나 잘못 접근하면 어떡하지”에 대한 걱정이 있었습니다. 시간 제한 때문에 논의가 깊이 이루어지지 못할까 봐 우려되기도 했습니다. 하지만 스터디를 진행하면서 논의 자체가 새로운 시각을 얻고 사고를 확장하는 데 중요한 과정이라는 걸 느꼈습니다. 모르는 것을 솔직하게 말하고 질문하는 것이 중요하다는 점을 깨달았고, 그 이후로 더 많은 도움이 되었습니다.