안녕하세요! 그로스로그 입니다!😊 🌱
"클린 코드를 지향할수록 구현 능력이 떨어진다"는 물음에서 시작한 강연 정리, 그리고 스레드가 겹쳐 생긴 버그를 막은 기록. 2기 6회차입니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.
01
🧹 클린 스프링: 스프링 개발자를 위한 클린 코드 전략
오연수 님
2024 인프콘에서 진행된 이일민(토비) 님의 클린 코드 강연을 듣고 정리한 글입니다.
시작이 도발적이에요. "클린 코드를 지향할수록 구현 능력이 떨어지는 것 같지 않나요?" 개발자라면 한 번쯤 품어본 의심입니다.
강연은 그 의심에 답하며 클린 코드의 진짜 목적, 기술 부채, 리팩터링, 그리고 팀워크까지 이어집니다. 정리 글이지만 흐름이 살아 있어요.
강연 정리 글은 자칫 목차 나열이 되기 쉽습니다. 그런데 이 글은 강연이 던진 질문과 그 답이 순서대로 남아 있어서, 읽으면 논리를 따라가게 됩니다.
조금 더 들어가 보면
강연의 출발점이 흥미롭습니다. "클린 코드에 집중할수록 구현 속도가 느려진다"는 개발자들의 체감이요.
이건 실제로 자주 생기는 일입니다. 좋은 설계를 고민하다 보면 코드를 못 씁니다. 인터페이스를 뽑을지 말지, 클래스를 어디까지 쪼갤지 고민하다가 하루가 갑니다. 원칙을 알수록 손이 느려지는 역설이죠.
강연은 여기서 원점을 짚습니다. '클린 코드'라는 말이 처음 나온 곳은 켄트 벡의 테스트 주도 개발이고, 원래 표현은 그냥 "클린 코드"가 아니라 "Clean Code That Works(작동하는 클린 코드)"였다는 것.
뒤의 두 단어가 빠지면서 의미가 달라진 겁니다. 작동하는 게 먼저고, 깨끗한 건 그다음이었는데, 순서가 뒤집힌 채로 원칙만 남은 거예요.
그리고 흔한 오해들을 정리합니다. 클린 코드를 추구하면 주석이 필요 없다, 코드가 깨끗하면 리팩터링할 이유가 없다, 테스트 코드도 필요 없다 — 전부 아니라고요.
🧭 유지보수성이 왜 특별한가
강연은 클린 코드의 특성을 네 가지로 정리하는데, 마지막 하나에 힘을 싣습니다.
읽기 좋은 코드, 이해하기 좋은 코드, 확장하기 좋은 코드, 그리고 유지보수하기 좋은 코드. 여기가 핵심이라는 겁니다.
왜일까요. 정리에 이런 문장이 있습니다. "유지보수성은 코드의 변경 가능성과 동의어"라고요.
품질에 관한 요구사항은 여러 가지입니다. 성능, 보안, 안정성, 호환성. 그런데 유지보수성만은 성격이 다릅니다. 유지보수하기 좋은 코드는 나머지 전부로 나아갈 수 있기 때문이에요. 느리면 고쳐서 빠르게 만들면 되고, 취약하면 보강하면 됩니다. 변경할 수 있으니까요.
반대로 바꾸기 어려운 코드는 어디로도 못 갑니다. 성능 문제가 있어도 손을 못 대고, 보안 이슈가 나와도 건드리기 무섭습니다. 그래서 유지보수성이 다른 품질의 전제 조건이 되는 거예요.
이어지는 생산성과의 관계 정리도 좋습니다. 둘은 서로를 깎아먹는 관계가 아니라 영향을 주고받는 관계이고, 균형이 중요하다는 것.
이 균형을 설명하는 그림이 선순환입니다. 유지보수성이 좋으면 변경하기 쉽고 → 변경하기 쉬우면 리팩터링이 잘 되고 → 빠르게 변경되는 코드는 생산성이 좋다. 한 바퀴가 돌아갑니다.
💳 기술 부채는 대충 짜라는 뜻이 아니다
가장 자주 오해받는 개념이 여기 나옵니다. 기술 부채요.
부채라는 비유는 워드 커닝햄이 만들었습니다. 원래 의미는 이렇습니다. 지금 이해한 만큼을 코드로 만들어 빠르게 내놓고, 나중에 더 알게 된 것을 리팩터링으로 반영하라.
핵심은 "지금 이해한 만큼"입니다. 처음엔 도메인을 다 모릅니다. 다 아는 척 설계하면 오히려 틀린 구조가 나와요. 그래서 현재 이해를 정직하게 반영한 코드를 내고, 배운 것을 반영하며 갚아나가는 겁니다.
정리에서 강조한 문장이 이겁니다. "부채는 나쁜 코드에 대한 변명이 될 수 없다." 대충 짜놓고 "나중에 갚죠"라고 하는 건 부채가 아닙니다. 진짜 부채는 처음부터 리팩터링하기 좋게 만들어둔 코드여야 성립해요. 갚을 수 없는 빚은 부채가 아니라 그냥 손실입니다.
🌌 그리고 삼체 문제
강연에서 가장 인상적인 비유가 마지막에 나옵니다.
유지보수성과 생산성, 두 개만 놓고 균형을 잡는 건 그래도 해볼 만합니다. 그런데 여기에 팀워크가 들어오면 이야기가 달라집니다.
강연은 이걸 삼체 문제에 빗댑니다. 천체가 둘일 때는 궤도를 계산할 수 있지만, 셋이 되는 순간 서로가 서로에게 영향을 주며 일반적인 해가 사라지는 문제요.
"나만을 위한 클린 코드는 세상에 없다"는 문장이 그래서 나옵니다. 클린 코드의 원칙 대부분은 함께 일하는 사람을 위한 것이고, 그래서 상황에 따라 기준이 달라집니다. 혼자 쓰는 스크립트와 열 명이 고치는 서비스는 같은 기준일 수 없죠.
강연의 결론이 기술이 아니라 태도로 끝나는 게 그래서입니다. "항상 친절하세요. 동료에게, 자신에게." 그리고 마지막 문장.
"클린 코드는 항상 코드에 관심을 가지고 보살피는 사람이 작성한 것처럼 보인다."
큐레이터 노트
듣고 흘리지 않고 구조로 남겼기 때문에 뽑았습니다.
좋은 강연을 듣고 나면 그때는 다 이해한 것 같습니다. 그런데 며칠 지나면 "클린 코드 얘기였는데…" 정도만 남아요. 이 글은 강연의 논리 흐름을 붙잡아뒀습니다. 오해 → 원점 → 유지보수성 → 부채 → 리팩터링 → 테스트 → 팀워크. 나중에 읽어도 다시 따라갈 수 있어요.
스프링 개발 원칙으로 착지시킨 부분도 좋았습니다. 자기 책임에 충실한 오브젝트, 유연한 의존관계, 계층 경계에는 인터페이스, DB 테스트에는 @Transactional. 추상적인 이야기를 자기가 매일 쓰는 프레임워크의 언어로 옮겨둔 거죠.
무엇보다 이 회차의 저자는 직전 회차에서 시큐리티 무한 루프를 디버깅한 그분입니다. 프레임워크 내부를 파고들던 사람이 그다음엔 "그래서 코드를 어떻게 써야 하는가"를 정리했어요. 기록이 쌓이면 방향이 보인다는 걸 잘 보여주는 흐름입니다.
02
🔒 Java 동기화
이태형 님
스프링부트에서 스케줄러를 동적으로 구현하다가 일부 스레드가 중복 실행되는 현상을 만납니다.
등록과 삭제는 정상, 생성되는 인스턴스도 문제없음. 원인을 못 찾아 일단
synchronized로 막기로 하고, 그 김에 자바 동기화를 정리한 글이에요.임계 영역, 모니터 락, 인스턴스 수준과 클래스 수준의 차이, 블록 동기화까지 예제 코드와 함께 짚습니다.
시작 문장이 솔직합니다. "원인을 해결 못 하고 synchronized를 사용해서 막아버리기로 결정". 실무에서 자주 있는 판단이고, 그 뒤에 공부가 따라온 게 이 글의 좋은 점입니다.
조금 더 들어가 보면
스레드가 여럿일 때 생기는 문제는 코드만 봐서는 잘 안 보입니다. 글의 예제가 그걸 잘 드러내요.
count가 5000에서 시작해 100씩 줄어드는 아주 단순한 코드입니다. count > 0일 때만 빼도록 조건도 걸려 있고요. 그런데 스레드 두 개가 동시에 돌리면 count가 이미 0인데도 계산이 이어집니다.
왜 그럴까요. if (count > 0)를 확인하는 순간과 실제로 빼는 순간 사이에 틈이 있기 때문입니다.
스레드 A가 "count가 100이네, 빼도 되겠다"라고 판단합니다. 그런데 실제로 빼기 전에 스레드 B가 끼어들어 100을 빼버립니다. 이제 count는 0이죠. 그 뒤 A가 자기 차례를 이어가며 또 100을 뺍니다. 결과는 -100입니다. 둘 다 조건을 확인했고, 둘 다 틀리지 않았는데 결과가 틀렸습니다.
이런 걸 경쟁 상태(race condition)라고 부릅니다. 무서운 건 재현이 잘 안 된다는 점이에요. 열 번 돌리면 아홉 번은 멀쩡합니다. 그래서 "테스트할 땐 됐는데 운영에서만 이상하다"가 나옵니다.
📝 원문에서는 이렇게 막습니다
해결은 한 단어입니다.
public synchronized void decrement() {
if (count > 0) {
count = count - 100;
System.out.println("Thread : " + Thread.currentThread()
+ ", Count : " + count);
}
}
synchronized가 붙으면 확인부터 빼기까지가 하나의 덩어리가 됩니다. 한 스레드가 이 메서드에 들어가 있는 동안 다른 스레드는 문 앞에서 기다려요. 중간에 끼어들 틈이 사라지니 결과가 예측 가능해집니다.
글은 여기서 더 나아가 잠금의 범위를 구분합니다. 이게 실무에서 중요한 부분이에요.
public void objectMonitorMethod() {
synchronized (this) {
// 이 블록 안의 코드만 동기화
}
// 여기는 동기화되지 않음
}
public void classMonitorMethod() {
synchronized (Sample.class) {
// 클래스 수준의 모니터
}
}
synchronized(this)는 같은 객체를 쓰는 스레드끼리만 막습니다. 객체가 다르면 서로 상관없이 동시에 돌아가요. 반면 synchronized(Sample.class)는 그 클래스의 모든 객체를 통틀어 하나만 들어갈 수 있게 합니다.
이 차이를 모르면 실수가 납니다. 공유 자원이 static인데 synchronized(this)로 막으면 막은 것 같지만 안 막힙니다. 객체가 여러 개면 각자 다른 문을 잠근 셈이니까요.
🧭 그리고 "막는 것"의 대가
글이 마지막에 짚는 주의사항이 중요합니다.
첫째, 잠금 자체에 비용이 듭니다. 그래서 네트워크 호출이나 파일 입출력처럼 오래 걸리는 작업은 가급적 잠금 구간 밖으로 빼야 합니다. 오래 잡고 있을수록 다른 스레드가 그만큼 놀게 되니까요.
둘째, 교착 상태(DeadLock)입니다. A가 1번 자물쇠를 쥔 채 2번을 기다리고, B가 2번을 쥔 채 1번을 기다리면 둘 다 영원히 멈춥니다. 잠금이 늘어날수록 이 위험도 커져요.
그래서 동기화는 넓게 걸수록 안전하지만 느리고, 좁게 걸수록 빠르지만 위험한 맞바꿈입니다. 글이 인스턴스 수준·클래스 수준·블록 단위를 나눠 정리한 이유가 여기 있습니다. 어디까지 막을지를 고르는 게 곧 설계예요.
큐레이터 노트
"일단 막고 공부했다"는 순서가 정직해서 뽑았습니다.
이상적으로는 원인을 끝까지 찾아 고치는 게 맞습니다. 그런데 현실에서는 마감이 있고, 동시성 버그는 재현조차 어렵습니다. synchronized로 막아 서비스를 안전하게 만들고, 그다음에 개념을 정리한 것 — 순서가 실무적이에요.
그리고 이 정리가 있으면 다음이 달라집니다. 지금은 "일단 막았다"지만, 모니터 락의 수준을 이해했으니 나중에 범위를 좁혀 성능을 되찾는 판단을 할 수 있게 되죠. 임시 조치를 지식으로 바꿔둔 겁니다.
스케줄러 중복 실행이라는 상황도 흔히 만나는 문제입니다. 서버가 여러 대면 각 서버가 같은 작업을 동시에 돌리기도 하고요. 그때 synchronized가 한 서버 안에서만 유효하다는 것까지 알아두면 좋습니다. 이 글은 그 첫 단추를 잘 꿰어놨어요.
💡 두 편을 겹쳐 읽으며
한 편은 강연 정리, 한 편은 버그 대응입니다. 결이 꽤 달라 보이죠.
그런데 두 글이 같은 이야기를 하는 지점이 있습니다. 둘 다 "지금 할 수 있는 것"과 "나중에 갚을 것"을 나눕니다.
토비 님의 강연은 기술 부채를 그렇게 설명합니다. 지금 이해한 만큼으로 만들어 내놓고, 배운 것을 리팩터링으로 반영하라고요. 완벽하게 만들려다 시작 못 하는 것보다 낫다는 겁니다.
이태형 님의 글은 그걸 실제로 합니다. 원인을 못 찾았지만 synchronized로 막아 문제를 멈추고, 대신 왜 이게 통하는지를 공부해 기록했어요. 나중에 원인을 찾으면 범위를 좁힐 수 있는 준비를 해둔 셈입니다.
중요한 건 "임시로 막았다"가 어디에 남느냐입니다. 아무 데도 안 남으면 그건 그냥 잊힌 코드가 되고, 다음 사람이 이유 없이 걸린 synchronized를 보며 고민하게 됩니다. 반대로 왜 막았는지가 기록되어 있으면 그건 갚을 수 있는 부채가 돼요.
성장일지가 하는 일이 정확히 그겁니다. 오늘의 판단에 이유를 붙여두는 것.
이번 회차에서 가져갈 것
1. 완벽한 설계보다 "바꿀 수 있는 코드"가 먼저. 유지보수성이 다른 품질의 전제입니다. 지금 성능이 아쉬워도 고칠 수 있는 구조라면 나중에 좋아질 수 있어요. 시작은 익숙한 기술로, 핵심 기능이 동작하는 가장 단순한 코드부터.
2. 임시 조치에는 반드시 이유를 남기기. 급하게 막은 코드는 나쁘지 않습니다. 왜 막았는지가 안 남은 게 나쁩니다. 주석 한 줄이든 기록 한 편이든, 근거가 있으면 나중에 갚을 수 있는 부채가 됩니다.
3. 동시성 버그는 "잘 되는 것"을 믿지 말기. 경쟁 상태는 열 번 중 아홉 번 멀쩡합니다. 여러 번 돌려서 안 났다는 게 안전하다는 뜻이 아니에요. 공유 자원을 건드리는 코드는 눈으로 짚어서 확인해야 합니다.
4. 잠금은 "어디까지 막을지"가 설계. synchronized(this)와 synchronized(Class.class)는 다릅니다. 공유 자원이 무엇인지 먼저 정하고, 딱 그만큼만 막으세요. 넓으면 느려지고, 어긋나면 안 막힙니다.
좋은 글 남겨주신 오연수 님, 이태형 님께 감사드립니다! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.