티스토리 뷰
이번에는 클린 코더(Clean Coder)라는 책을 읽고, 중요하다고 생각되는 내용들을 정리해보도록 하겠습니다.
1. 프로의 마음가짐
[ 함부로 바라지 마라 ]
프로의 마음가짐, 프로페셔널리즘(Professionalism)이라는 용어에는 숨은 뜻이 있다. 그것은 명예와 긍징의 상징인 동시에 책임과 의무를 나타내는 것이다.
프로가 아니라면 세상을 살기 편해진다. 하는 일에 책임을 느낄 필요가 없고, 회사에 책임을 떠넘겨도 되기 때문이다. 하지만 프로는 실수하면 스스로 뒷감당을 해야 한다.
[ 기능에 해를 끼치지 마라 ]
오류를 만들면 안된다.
오류를 만들면 기능에 해가 된다. 따라서 프로가 되려면 오류를 만들면 안된다. 하지만 소프트웨어는 너무 복잡해서 오류가 생길 수 밖에 없다. 그렇다고 너무 복잡하다는 이유로 책임이 사라지지는 않는다.
그러므로 우리는 완벽하지 않다는 사실에 책임을 져야 한다. 완벽한 소프트웨어를 만드는 일이 사실상 불가능하다는 것이지 완벽하지 않아도 괜찮다는 것은 아니다. 그러므로 프로라면 오류에 책임을 져야 하며, 우선 사과하는 법을 익혀야 한다. 사과는 필수 요소지만 그것만으로는 충분하지 않다. 같은 오류를 반복하면 안되고, 경력을 쌓아가면서 오류를 만드는 비율을 0에 가깝게 떨어뜨릴 수 있어야 한다.
QA는 아무것도 찾지 못해야 한다.
소프트웨어를 출시할 때는 QA가 문제를 찾지 못할 것이라고 자신할 수 있어야 한다. 코드에 결함이 있는 것 알면서도 QA에게 코드를 보내는 것은 매우 프로답지 못하다. 여기서 결함이 있는 코드란 확신을 갖지 못하는 코드 전부를 의미한다.
어떤 이들은 QA를 오류를 찾는 용도로 사용한다. 이러한 일은 그저 게으르고 무책임할 뿐이다.
하지만 우리가 놓친 오류들을 QA는 결국 찾을 것이다. QA가 문제를 찾을 때마다, 더 나쁜 경우 사용자가 문제를 찾을때 마다 놀라움과 분함을 느껴야 마당하며, 다시는 그런 일이 생기지 않도록 마음을 다져야 한다.
제대로 작동하는지 아닌지 알아야 한다.
코드가 잘 돌아가는지 아는 방법은 테스트하고 또 테스트하는 것이다. 하지만 테스트를 계속 하면 시간이 많이 걸리고 일정을 맞추지 못할 수 있다.
그러므로 우리는 테스트를 자동화해야 한다. 빠르게 실행가능한 테스트를 만들고 가능한 자주 돌려야 한다. 얼마만큼의 코드를 자동화한 단위 테스트를 작성해야 하느냐? 대답할 필요조차 없이 모조리 해야 한다.
100%의 테스트 커버리지를 권장하는게 아니고, 강력히 요구한다. 실행된다면 제대로 돌아가는지 알아야 하는데, 방법은 테스트 뿐이다.
만약 테스트가 어려운 코드가 있다면, 코드를 테스트하기 어렵게 설계했기 때문이다. 해결책은 테스트하기 쉽게 코드를 설계하는 것이고, 가장 좋은 방법은 테스트 코드를 먼저 작성하는 테스트 주도 개발(TDD, Test-Driven Development)로 개발하는 것이다.
[ 구조에 해를 끼치지 마라 ]
전체 구조를 희생하면서까지 기능을 추가하는 일은 헛수고라는 것을 프로라면 당연히 알고 있다.
구조가 좋아야 코드가 유연해지는 것이며, 구조가 위태로우면 미래도 위태롭다.
소프트웨어는 변한다라는 생각은 모든 소프트웨어 프로젝트의 기본 가정이다. 이 가정을 어기고 구조를 유연하게 만들지 못하면 수익 기반인 경제 모델이 취약해진다.
즉, 변경을 할 때 터무니없는 비용을 치르지 않고 변경할 수 있어야 한다. 하지만 대부분의 프로젝트는 형편없는 구조 때문에 허덕이게 된다. 하루면 끝날 작업은 일주일이 걸리게 되고, 관리자들은 속도를 빠르게 하고자 개발자들을 더 고용한다. 하지만 이들 역시 곤란한 상황만 더하고 구조에 큰 피해를 입히며 장애물을 만들 뿐이다.
이러한 상황을 피하기 위해 우리는 모듈을 살펴볼 때 마다 작고 가벼운 변화를 더해 구조를 개선해야 한다. 코드를 읽을 때마다 구조를 바꿔야 한다.
누군가는 동작 중인 소프트웨어를 바꾸는 일이 위험하다고 생각한다. 하지만 이는 사실이 아니다. 코드를 바꾸는 것을 두려워하는 이유는 망가트릴까바 이며, 이는 테스트가 없기 때문이다. 만약 자동화된 테스트 묶음(suite)의 커버리지가 100%이며, 재빨리 돌려볼 수 있다면 코드를 바꾸는 일은 전혀 무섭지 않다.
오히려 정말 위험한 일은 소프트웨어를 고장난 상태로 두는 것이다.
[ 전산 분야 지식을 익혀라 ]
다음은 프로 소프트웨어 개발자라면 알아야 하는 최소한의 기술 목록이다.
- 디자인 패턴: 24가지 GOF 패턴을 설명할 수 있고, POSA 패턴을 실무에 적용할 수준으로 알아야 한다.
- 설계 원칙: SOLID 객체 지향 원칙을 알아야 하고, 컴포넌트 개념을 충분히 이해해야 한다.
- 방법론: XP, 스크럼, 린, 칸반, 폭포수, 구조적 분석, 구조적 설계 개념을 충분히 이해해야 한다.
- 원칙: 테스트 주도 개발, 객체지향 설계, 구조적 프로그래밍, 지속적 통합, 짝 프로그래밍을 실천해야 한다.
- 도구: UML, 데이터 흐름도, 구조 차트, 페트리 넷, 상태전이 다이어그램과 테이블, 흐름도, 결정 테이블을 어떻게 쓰는지 알아야 한다.
[ 끊임없이 배우기 ]
연습하기
프로는 연습한다. 업종에 무관하게 진정한 프로는 기술을 날카롭게 갈고 닦아 항상 준비된 상태로 만들려고 애쓴다. 소프트웨어 개발자들은 프로그래밍을 통해 훈련을 계속해서 반복해야 한다. 간단한 알고리즘 문제를 푸는 형식도 괜찮다. 아침과 저녁에 짧은 시간 프로그래밍을 하는 것은 도움이 된다.
함께 일하기
배움에 도움이 되는 또 다른 방법은 다른 사람들과 함께 일하는 것이다. 그러면 서로 많이 배울 뿐 아니라 일도 빨리 끝나고 오류가 더 적다.
멘토링
배우기에 가장 좋은 방법은 가르치는 것이다. 가르치고 배울 때 더 큰 이득을 보는 쪽은 가르치는 사람이다.
마찬가지로 새로운 사람을 조직에 익숙하게 만드는 가장 좋은 방법은 옆에서 작업 요령을 알려주는 것이다. 프로라면 후배들을 멘토링해야 하고, 방치된 채 이리저리 떠돌게 둬서는 안된다.
겸손
프로그래밍은 무에서 유를 창조하는 행위이다. 프로는 자신의 일을 이해하고, 그 일에 자부심을 가진다. 프로는 자기 능력을 확신하고, 그 확신을 바탕으로 위험을 과감히 짊어진다.
그러나 프로는 때때로 실패하고, 언젠가 자신의 능력이 부족해지는 날이 온다는 것을 안다. 그리고 프로는 자신이 웃음거리가 됐을 때 가장 먼저 웃어야 한다. 절대 다른 사람을 비웃지 않지만 자신이 비웃음거리가 될 만하다면 받아들여야 한다. 그렇다고 다른 사람은 당신이 실수를 했다고 망신을 주지 않을 것이다. 왜냐하면 다음 번 실패할 사람이 자신이 될 수 있기 때문이다.
프로는 목표가 무너졌을 때 웃고, 이를 받아들일 수 있어야 한다.
2. 아니라고 말하기
[ 반대하는 역할 ]
프로라면 권위에 맞서 진실을 말해야 한다. 프로는 관리자에게 아니라고 말하는 용기를 가져야 한다.
무리한 요구 또는 일정에 아니라고 말하는 일이야 말로 맡은 작업을 완료하는 유일한 길이다.
[ 함부로 바라지 마라 ]
이해관계가 높을 때야 말로 아니라고 말할 가장 중요한 순간이다. 이익과 손해가 클수록, 아니라는 말의 가치도 높아진다. 만약 실패하면 회사의 손해가 너무 커서 생존 여부가 달려있다면, 마음을 궂게 먹고 관리자에게 알려야 한다.
[ 약속을 뜻하는 말]
우리는 언제나 예라고 약속하고 싶다. 하지만 예라고 말하기 위해서는 진심을 담고 실행해야 한다. 약속을 하는 행동은 세 부분으로 나뉜다.
- 하겠다고 말한다.
- 진심을 담는다.
- 실제로 행동한다.
하지만 이 세 단계를 거치지 않는 사람들을 실제로 매우 자주 만날 수 있다.
[ 약속이 부족함을 알아차리기 ]
- 다른 사람 또는 팀에 의존하는 경우: 자신이 모든 상황을 제어할 수 있을 경우에만 약속을 해야 하며, 끝내겠다는 약속이 아닌 끝내기 위한 구체적인 노력들을 약속해야 한다.
- 인프라 팀을 통해 의존하는 모듈에 대한 얘기를 나눈다.
- 인터페이스를 만들어 다른 팀의 의존성을 추상화한다.
- 빌드 담당자를 만나 변경 사항이 빌드 시스템에서 잘 돌아가는지 화ㅓㄱ인한다.
- 개인 빌드를 만들어 모듈 통합 테스트를 실행한다.
- 방법을 모르는 경우: 방법을 모를 경우에는 어떻게든 하겠다는 무책임한 약속 보다는 방법을 찾아내기 위해 다음과 같은 것들을 하겠다고 약속할 수 있다.
- 출시 전 남은 25개 오류를 모두 조사하고 재현한다.
- 각 오류를 발견한 QA와 함께 오류를 재현한다.
- 이번 주에는 오류 수정에만 전념한다.
- 예상치 못한 일이 발생한 경우: 살다보면 예상치 못한 일이 일어나기도 한다. 그래도 예상했던 목표를 맞추고 싶다면, 지금 당장 예상치를 바꿔야 한다. 약속을 못지키겠다면 약속한 사람에게 붉은 깃발을 휘둘러야 한다.
- 정해진 회의 시간에 맞춰 가는데, 차가 막힐지도 모른다면 즉시 동료에게 전화를 걸어 알려야 한다.
- 해결할 만 해보였던 오류가 생각보다 심각하다면, 깃발을 들고 약속을 지켜 오류를 고칠지 또는 우선순위를 바꿔 다른 간단한 오류부터 해결할 지 정해야 한다.
3. 코딩
[ 준비된 자세 ]
코딩은 어려운데다 사람을 지치게 하는 지적 활동이다. 다른 훈련에서는 찾기 힘든 일정 수준의 농축된 집중력이 필요하다.
- 코드는 반드시 동작해야 한다.
- 풀고자 하는 문제가 어떤 문제이고 어떻게 풀어야 하는지 확실히 이해해야 한다.
- 그리고 해결법을 나타낸 코드에 믿음이 가는지 확인해야 한다.
- 코드는 고객이 제시한 문제를 반드시 풀어야 한다.
- 고객의 요구사항이 고객의 무넺를 해결하는데 도움이 안되는 경우도 있다.
- 이런 상황을 파악하고 고객과 협상해 고객의 진정한 필요를 충족시키는 일은 본인에게 달려있다.
- 코드는 기존 시스템에 잘 녹아들어야 한다.
- 기존 시스템의 경직성, 취약함, 불투명도를 높이면 안되고, 의존성도 잘 관리되어야 한다.
- 즉, 코드는 SOLID 객제지향 원칙을 따라야 한다.
- 코드는 다른 프로그래머가 읽기 쉬어야 한다.
- 주석을 잘 쓰라는 단순한 조언이 아니다. 코드에는 작성자의 의도가 잘 드러나도록 다듬어야 한다.
- 이는 프로그래머가 통달하기 가장 어려운 일이다.
충분히 강하게 집중하지 못하면 잘못된 코드를 만들게 된다. 코드에 오류가 섞이고 구조는 엉뚱해지고, 이해하기 힘든 뒤얽힌 코드가 된다. 만약 지치거나 주의력이 흩어졌다면 코드를 만들지 말자. 결국 재작업이 필요할 것이다.
[ 새벽 3시에 짠 코드 ]
지쳤을 때는 코드를 만들지 않는 것이 좋다. 그렇지 않으면 잘못된 설계 구조와 오류들로 나중에 고생하게 될 것이다.
[ 근심이 담긴 코드 ]
외적인 근심이 있을 때 작성한 코드들은 모두 쓰레기다. 그럴 때는 코딩을 하는 대신 근심을 풀어야 한다.
물론 회사에서 이러한 부분을 용납하지 않을 수 있다. 그럴 때는 시간을 나누어 일정 시간에는 근심을 풀고, 남은 시간에는 업무를 해야 한다.
물론 가장 좋은 것은 개인적인 문제들은 개인 시간에 힘쓰는 것이다.
[ 몰입 영역 ]
몰입으로 알려진 상태는 코딩을 하는 동안 빠져드는 고도로 집중하는 상태이다. 하지만 이러한 몰입 상태는 오히려 생산적이지 못하고, 지나치게 몰두한 나머지 이성적 판단을 못하게 된다.
[ 외부 방해 ]
코딩을 하다가 다른사람이 질문을 하면 개발 흐름이 끊기게 된다. 이를 위해서는 짝 프로그래밍이나 TDD가 도움이 된다. 짝 프로그래밍은 내가 흐름을 놓쳐도 짝이 흐름을 가지고 있기 때문이며, TDD는 테스트를 통해 작업의 흐름을 유지할 수 있기 때문이다.
[ 디버깅 시간 ]
디버깅 시간은 TDD 원칙을 받아들이면 엄청나게 단축할 수 있다.
TDD가 아닌 다른 효과적인 원칙을 받아들이건 디버깅 시간을 가능한 0에 가깝게 줄이는 것이 프로의 의무이다. 0은 명백히 불가능하겠지만 목표는 목표이다.
[ 속도 조절 ]
소프트웨어 개발은 마라톤이지 단거리 질주가 아니다. 시작부터 있는 힘껏 달리는 것이 아니라 자원을 보존하고 속도를 조절해야 한다.
예를 들어 풀고 있는 문제를 다 못풀었다면 집에 못가는 것이 아니고 가야한다. 피곤하면 창의성과 총명함이 사라진다. 그 상태에서 문제를 풀려고 하는 것은 어떠한 도움도 되지 않는다.
[ 속도 조절 ]
언젠가는 마감을 못 지키는 날이 온다. 일정 지연을 관리하는 요령은 이른 감지와 투명성이다. 일정을 지키지 못할 것 같으면 바로 관계자들에게 알려라.
그리고 마감을 지키라는 압박이 들어와도 동의하면 안된다.(물론 이는 일정에 대해 진지하게 고민했을 때의 이야기다.) 이는 잘못된 희망을 통해 재앙으로 가게 한다. 일정에 맞추는 방법은 범위를 줄이는 것 뿐이다.
4. 테스트 주도 개발
[ 배심원 등장 ]
TDD에 대한 논쟁은 블로그나 기사를 통해 수년간 계속 되었지만, TDD는 결국 잘 돌아가고 있으며 이를 우리는 받아들여야 한다.
일방적으로 느껴질 수 있지만, 연구자료를 봤을 때 프로그래머는 TDD를 적용해야 한다고 생각한다.
프로라면 작성한 코드가 전부 문제가 없는지를 알아야 하고, 테스트가 없다면 전부 문제가 없는지 파악하기가 힘들다. 그리고 TDD를 사용하지 않는다면 높은 커버리지를 갖는 자동화된 단위 테스트를 만들기가 어렵다.
[ TDD의 3가지 법칙 ]
- 실패한 단위 테스트를 만들기 전에는 제품 코드를 만들지 않는다.
- 컴파일이 안 되거나 실패한 단위 테스트가 있으면 더 이상의 단위 테스트를 만들지 않는다.
- 실패한 단위 테스트를 통과하는 이상의 제품 코드는 만들지 않는다.
이 3 가지 법칙을 지키면 반복 주기는 대략 30초 길이를 유지한다. 처음에는 작은 단위 테스트를 만들면서 시작한다. 하지만 몇 초 지나지 않아 아직 만들지도 않은 클래스나 함수의 이름을 써야 하고, 그 때문에 컴파일조차 되지 않는다. 따라서 테스트가 컴파일 되도록 제품 코드를 만들어야 한다. 하지만 그 이상의 제품 코드를 만들면 안되므로, 새로운 단위 테스트를 만들어야 한다.
TDD는 이렇게 테스트를 조금 추가하고, 제품 코드를 조금 추가하는 주기를 반복한다.
[ 수많은 혜택 ]
확산
TDD를 프로다운 규칙으로 받아들이면 테스트를 하루에 수 십개 이상 만들게 된다. 그리고 코드가 수정되거나 추가되면 작성한 테스트를 통해 제품에 대한 확신을 얻을 수 있다.
결함 주입 비율
TDD로 개발하면 코드 추가 시 새로운 결함을 만드는 비율인 결함 주입 비율이 매우 낮다. 이는 단발적인 효과가 아니다.
용기
나쁜 코드를 봐도 고치지 않는 이유는 망가뜨릴지도 모른다는 위험 때문이다. 수정하였다가 망가지면 자신이 감당해야 하기 대문이다.
하지만 TDD를 한다면 믿음직한 테스트 묶음(Suite)가 있기 때문에 변경에 대한 두려움이 사라진다. 이를 통해 깔끔한 코드를 작성해 이해하기 쉽고 확장하기 쉽게 만든다. 또한 코드가 단순해져 오류가 생길 가능성이 줄어든다.
문서화
단위 테스트는 코드로 만든 예제이며, 시스템을 어떻게 사용해야 하는지 알려준다. 즉, 단위 테스트는 문서이며 시스템의 가장 낮은 단계의 설계를 알려준다. 테스트는 저수준 단계에서 가장 훌륭한 문서의 형태이다.
설계
3가지 단계(테스트 코드 → 제품 코드 → 리팩토링)에 따라 테스트를 만들다 보면 딜레마에 빠진다. 어떤 코드를 만들어야 하는지 정확히 아는데도 불구하고, 법칙을 따르려다 보니 제품 코드가 없어 실패하는 단위 테스트가 필요하기 때문이다.
테스트 코드를 만들기 위해서는 코드의 의존 관계를 고립시켜야 한다는 어려움이 있다. 다른 함수를 호출하는 함수는 테스트하기 어려운 경우가 많다. 그러면 우리는 자연스럽게 고민을 하게 되고, 결과로 의존성이 낮은 좋은 설계로 이어지게 된다.
프로다운 선택
TDD는 여러 장점들을 가져다 주는 원칙으로 TDD를 사용하는 것은 매우 프로다운 선택이라는 것이다.
[ TDD와 관련없는 사실 ]
TDD는 많은 장점이 있지만 종교나 마법이 아니다. TDD로 개발하여도 형편없는 코드가 나오기도 한다. 물론 이런 경우는 드물지만 없지는 않다. 프로 개발자라면 좋은 점보다 해로운 점이 많을 때 규칙을 따라서는 안된다.
5. 테스트 전략
[ QA는 오류를 찾지 못해야 한다. ]
회사에 소프트웨어를 테스트하는 QA팀이 있더라도, 개발팀은 QA가 문제를 찾지 못하는 상태를 목표로 삼아야 한다. 물론 마음먹었고 쉽게 달성할 수 있는 목표는 아니다.
아무리 작업을 하여도 QA는 문제를 찾을 것이고, QA가 문제를 발견할 때마다 개발팀은 간담이 서늘해져야 정상이다. 그리고 오류가 어떻게 발생했는지 파악하여 다시 발생하지 않도록 조치해야 한다.
QA는 같은 팀이며, QA와 개발자는 시스템 품질 향상을 위해 힘을 합쳐야 한다.
[ 테스트 자동화 피라미드 ]
프로 개발자는 단위 테스트를 만들기 위해 테스트 주도 개발 원칙을 따라야 하며, QA가 오류를 찾지 못하리라는 확신을 가지려면 상위 계층 테스트 또한 필요하다. 테스트들은 다음과 같은 순서로 구성된다. 갈수록 더 상위 테스트에 해당된다.
- 단위 테스트
- 컴포넌트 테스트
- 통합 테스트
- 시스템 테스트
- 탐색 테스트
단위 테스트
단위 테스트는 피라미드의 가장 아래 계층에 존재한다. 단위 테스트는 프로그래머에 의해 프로그래머를 위해 시스템 프로그래밍 언어로 만든 테스트이다. 단위 테스트는 시스템의 최하위 계층을 명세하는 의도로 만들어지며, 커버리지는 최대한 100에 가까워야 한다.
컴포넌트 테스트
컴포넌트 테스트는 인수 테스트의 일종으로, 보통 컴포넌트 테스트의 대상은 시스템의 독립 컴포넌트이다.
컴포넌트 테스트는 컴포넌트에 입력 값을 넣고 출력 값을 받아 모으고, 입력에 대한 출력이 올바른지 테스트한다. 테스트 대상이 아닌 컴포넌트는 적절한 mocking 또는 대역을 사용하여 테스트로부터 분리한다.
컴포넌트 테스트는 QA와 사업부가 개발팀의 도움을 받아 작성하며 시스템의 절반 정도를 감당한다.
통합 테스트
통합 테스트는 여러 컴포넌트로 이루어진 큰 시스템에만 의미가 있다. 통합 테스트는 컴포넌트 묶음을 모아서 묶음끼리 제대로 상호작용하는지 테스트한다. 테스트 대상이 아닌 컴포넌트는 적절한 mocking 또는 대역을 사용하여 테스트로부터 분리한다.
통합 테스트는 대개 시스템 아키텍트나 수석 설계자가 만들며, 성능 테스트나 처리량 테스트가 진행되기도 한다. 통합 테스트는 시간이 많이 들기 때문에 지속적 통합때는 수행하지 않으며, 주기적으로 필요할 때마다 돌린다.
시스템 테스트
시스템 테스트는 통합한 시스템 전체를 대상으로 하는 자동화 테스트이다. 즉, 궁극적인 통합 테스트이다. 시스템이 올바르게 연결 되었고, 각 부분이 계획에 따라 상호작용하는지 테스트한다. 보통 성능이나 처리량 테스트는 시스템 테스트 단계에서 실행된다.
시스템 테스트는 시스템 아키텍트나 기술 책임자가 만들며, 실행 속도에 비해 자주 돌리는 편이지만 자주 돌리면 좋다.
시스템 테스트는 대략 시스템의 10%를 감당한다. 시스템이 올바르게 동작하는지보다 시스템을 올바르게 빌드했는지를 확실히하려는 목적이기 때문이다.
수동 탐색 테스트
수동 탐색 테스트는 키보드에 손을 얹고 모니터를 보며 직접 하는 테스트이다. 자동화된 테스트가 아니며, 스크립트로된 테스트도 아니다. 이 테스트의 목적은 시스템이 기대한 대로 동작하는지 확인하고, 예상치 못한 오류를 찾아내는 것이다. 목표 달성을 위해서는 인간의 두뇌와 창의력이 필요하다.
어떤 팀은 모든 관계자를 불러 시스템을 마구 두드려 보라고도 한다. 수동 탐색 테스트의 목적은 커버리지가 아니고, 모든 실행 경로를 빠짐없이 검사하여 가능한 많은 특이사항을 창의적으로 발견하는 것이기 때문이다.
'나의 공부방' 카테고리의 다른 글
[OOP] 객체지향 캡슐화(Encapsulation), 응집도(Cohension)와 결합도(Coupling) (0) | 2021.11.09 |
---|---|
[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID (38) | 2021.11.02 |
[TDD] 단위 테스트와 TDD(테스트 주도 개발) 프로그래밍 방법 소개 - (1/5) (26) | 2021.08.16 |
[클린코드] 변경을 최소화하는 개발, 관심사의 분리와 변하는 것과 변하지 않는 것의 분리 (0) | 2021.06.30 |
[디자인 패턴] 싱글톤이 안티 패턴이 될 수 있는 이유와 자바 싱글톤과 스프링 싱글톤의 차이 (6) | 2021.05.14 |