7년 전부터 미루고 미루다 읽은 책..
처음에는 학교 선배가 추천해준 책이었는데, 대학생때 워낙 바쁘게 살다 보니 사놓고 읽지는 못했던 그런 책 중에 하나였다. 예전부터 깔끔한 코드에 관심이 있기도 했고 오픈채팅방이랑 스터디에서 지속적으로 언급되는 책이어서 호기심이 커져서, 구매 7년 만에 표지를 넘길 수 있었다.
주워들은 지식으로 알고 있었던, 하지만 알고만 있었던(?) 내용
기술 블로그나 코딩컨벤션을 정할 때 생각하던 내용들을 왜 그런지, 예시와 함께 설명해주는 책이었다. 학문적인 부분은 대학생때 배운 내용들이 대부분을 차지했다. SOLID, DRY, KISS, Critical Section, Dead Lock, Live Lock 등등..
자바는 대학 다닐때 4년 동안 제일 좋아했던 언어였지만, 첫 취업을 JS계열로 커리어를 쌓게 되면서, 졸업 이후에는 자바 코드를 본 적이 없었지만 언어는 언어일 뿐이라서 막 이해가 안 될 정도는 아니었다.
- 클린 코드는 "팀에서 서로 동의하는 원칙"을 세우기 위한 소통에서 시작된다.
- 개발 생태계에서 라이브러리와 프레임워크가 빠르게 발전하지만, 개발에 대한 원칙과 기본은 변하지 않는다.
The boy scout rule. 떠날 때는 찾을 때보다 더 깨끗하게 해야 한다. 이는 개발에서도 적용된다. > git pull
로 체크아웃한 코드보다 > git push
로 체크인 한 코드가 더 깨끗해야 한다.
가장 좋은 건, 프로그램을 잘 짜는 사람과 같이 일하는 것이다. 그 사람이 생각하는 방식을 배운다면 개발 실력이 순식간에 껑충 뛴다. 개발 실력은 계단식으로 성장하는데, 아무런 자극이 없으면 그 단계를 벗어나지 못한다.
SW는 80% 이상이 유지보수다. 건축 업계의 수리공이나 자동차 업계의 수리공처럼 소프트웨어 개발자를 생각해야 한다.
읽기 좋은 코드는 돌아가는 코드만큼이나 중요하다. 코드를 인정사정없이 리팩토링하라.
아이 이름을 짓듯이 심사숙고해서 지어야 한다.
설계는 과정이지, 고착된 종착점이 아니다.
기계가 실행할 정도로 상세하게 요구사항을 명시하는 작업. 바로 이것이 프로그래밍이다.
Leblanc's Law. 나중은 결코 오지 않는다. 돌아가는 쓰레기에 대한 리팩토링을 미루지 마라.
시간을 들여 깨끗한 코드를 만드는 노력이 장기적인 비용을 절감하는 방법이며, 전문가로서 살아남는 길이다.
대다수 관리자는 좋은 코드를 원한다. 그러면서도 관리자가 일정과 요구사항을 강력하게 밀어 붙이는 이유는, 그것이 관리자의 책임이기 때문이다. 개발자의 책임은 좋은 코드를 사수하는 일이다.
빠르게 가는 유일한 방법은 언제나 코드를 최대한 깨끗하게 유지하는 습관이다.
저자는 독자들과 소통할 책임이 있으며, 한 줄의 문장을 글 중간에 삽입하려면 끊임없이 기존 글을 읽는다. 뒤로 갈수록 쓰는 시간보다 읽는 시간이 많아질 것이기 때문에 술술 읽혀야 한다.
변수명은 의도가 분명해야 한다.
개발자에게 List는 특수한 의미다. 실제 List 추상 자료형이 아니라면, accountList처럼 명명하지 않는다.
Product라는 클래스가 있는데, ProductInfo나 ProductData라는 클래스를 다루면 안 된다. 다 같은 의미로 인지하기 때문이다.
프로그래밍은 사회 활동이기 때문에 발음하기 쉬운 이름은 중요하다. 명료하고 간결하게 지어라.
i, j 같은 변수가 아닌 realDaysPerIdealDay, WORK_DAYS_PER_WEEK 같은 변수명을 사용하라.
접두어 등의 인코딩을 피하라. 헝가리안 표기법에서의 데이터 타입을 앞에 적는 방식이나 인터페이스의 경우 'I'를 붙이는 방식은 피하라. 이는 불필요한 부담이 된다.
클래스 이름과 객체 이름은 명사나 명사구가 적합하다. Customer, WikiPage, Account, AddressParser등이 좋은 예이며, 피해야 할 단어로 Manager, Processor, Data, Info 등이 있다.
메서드 이름은 동사나 동사구가 적합하다. postPayment, deletePage, save등이 좋은 예이며, 접근자(Accessor) • 변경자(Mutator) • 조건자(Predicate)는 get, set, is를 사용한다.
동일 코드 기반인데 어떤 것은 Controller, 어떤 것은 Manager로 짓지 마라. A와 B를 더하는 add로 써오다가 집합의 요소를 추가하는 add 메서드를 추가하지 마라.
함수를 만드는 첫째 규칙은 '작게!'다. 둘째 규칙은 '더 작게!'다.
함수는 작을 수록 좋다. 20줄도 길다. if/else/while문 등에 들어가는 블록은 한 줄이어야 하고, 대게 거기서 함수를 호출한다.
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
함수 내 모든 문장의 추상화 수준이 동일해야 한다. '내려가기 규칙'을 적용해서 논리적으로 아래와 같은 형태로 표현 되어야 읽는 사람이 헷갈리지 않는다.
- 제목1
- 소제목1
- 본문1
- 본문2
- 소제목2
- 소제목3
- 본문1
확장에 열려 있어야 하고, 수정에 닫혀 있어야 한다.
함수는 이름이 길어도 괜찮다. 겁먹을 필요 없다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다. 이름을 정하느라 시간을 들여도 괜찮다.
가장 이상적인 인수의 수는 0개다. 적으면 적을 수록 좋다. 3개는 피하는 편이 좋고, 4개 이상은 특별한 이유가 필요하다.
단항 인수 함수는 동사/명사 쌍을 이루면 의도를 명확히 표현할 수 있다. writeFile("name") 처럼 말이다. 아래는 좋은 예다.
- boolean fileExists("MyFile")
- InputStream fileOpen("MyFile")
- passwordAttemptFailedNtimes(int attempts)
아래는 안 좋은 예다.
- void includeSetupPageInfo(StringBuffer pageText)
변환 함수에서 출력 인수를 사용했다는 점, 혹은 객체 설정을 외부에 맡겼다는 점에서 혼란을 일으킨다.
함수로 부울 값을 넘기는 관례는 정말로 끔찍하다. SRP를 어긴다고 대놓고 공표하는 셈이다.
Point p = new Point(0,0)
는 좋은 예이지만, func(A,B)
는 A.method(B)
로 리팩토링하는 게 좋다.
이 경우는 대게 일부를 독자적인 클래스로 선언할 수 있다. Circle makeCircle(double x, double y, double radius);
는 Circle makeCircle(Point center, double radius);
로 바꿀 수 있다.
함수 선언부를 찾아보는 행위는 코드를 보다가 주춤하고 인지적으로 거슬린다는 뜻이므로 피해야 한다. 추상화된 이름으로 유추할 수 있도록 한다.
함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. if (deletePage(page) == E_OK)
는 좋은 코드가 아니다. 오류 코드 대신 Try/Catch 예외 처리를 사용하라.
중복은 소프으웨어에서 모든 악의 근원이다. AOP(Aspect Oriented Programming), COP(Component Oriented Programming) 등 모든 개발 방식이 중복 제거에 기반해 있다.
주석은 필요 없다.
코드로 의도를 표현하는 데에 실패하여 주석을 사용한다. 우리가 작성한 코드가 지저분한 모듈이라는 사실을 자각해서 "주석을 달아야겠다!" 라고 생각한다. 주석을 되도록 안 쓰는 코드가 클린코드다.
주석은 시간이 지날수록 코드에서 멀어진다. 부정확한 주석은 아예 없는 주석보다 훨씬 나쁘다.
지금은 Git 같은 VCS가 있기 때문에 옛날 코드처럼 변경 이력을 주석으로 기록하고 관리하지 않아도 된다. 완전히 제거하는 편이 좋다. 주석처리된 코드뭉치 또한 과감히 삭제하라.
오늘 구현한 기능이 다음 버전에서 바뀔 확률은 아주 높다.
코드 형식은 의시소통의 일환이다. 따라서 가독성은 앞으로 바뀔 코드의 품질과 속도에 지대한 영향을 미친다.
서로 밀접한 개념은 한 파일에 속해야 마땅하다. protected 변수를 피해야 하는 이유 중 하나다. 연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스 파일을 여기저기 뒤지게 된다.
변수는 사용하는 위치에 최대한 가까이 선언한다. 지역번수는 각 함수 맨 처음에 선언한다. 루프를 제어하는 변수는 루프 문 내부에서 선언한다.
인스턴스 변수는 클래스 맨 처음에 선언한다.
한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다.
행 길이는 100~120자 정도로 제한하는 게 좋다.
팀은 한 가지 규칙에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 개개인이 따로국밥처럼 맘대로 짜대는 코드는 피해야 한다. 어떤 개발자가 와도 한 소스파일에서 봤던 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 줘야 한다.
추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.
인터페이스는 자료 구조를 명백하게 표현해야 한다. 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현해야 한다.
새로운 자료 타입이 필요한 경우 객체 지향 기법이 적합하고, 새로운 함수가 필요한 경우 절차 지향 기법이 적합하다.
디미터 법칙은 잘 알려진 휴리스틱이다. 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다. 메서드가 반환하는 객체의 메서드는 호출하면 안 된다. 아래는 '기차 충돌'이라는 안 좋은 예시다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
그리고 아래는 좋은 예시다.
final String outputDir = ctxt.options.scratchDir.absolutePath;
DTO는 공개 변수만 있고, 함수가 없는 자료 구조체다.
활성 레코드는 객체가 아닌, 자료 구조체다. DTO의 특수한 형태로, 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다. 내부 자료를 숨기는 객체는 따로 생성하되, 이는 활성 레코드의 인스턴스일 가능성이 높다.
오류가 발생하면 예외를 던지는 게 좋다. 그러면 호출자 코드가 더 깔끔해진다.
예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 좋다. 그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.
먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 코드를 작성하는 방법을 권장한다. 그러면 트랜잭션 본질을 유지하기 쉬워진다.
예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다. Catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨 주고 로깅한 뒤, 프로그램을 계속 수행해도 좋은지 확인한다.
외부 API를 사용할 때는 감싸기 기법이 최선이다. 의존성이 낮아서 나중에 다른 라이브러리로 갈아타도 비용이 적으며, 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다.
때로는 외부 API를 감싸는 게 적합하지 않은 때도 있다. 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하여 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다. 이를, 특수 사례 패턴이라고 부른다.
null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘기고, null을 호출자가 처리하게 되면 종속성이 강해진다. null을 반환하고픈 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환한다. 만약 사용하려는 외부 API가 null을 반환한다면 감싸기 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하는 방식을 고려한다.
null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다. 대다수 프로그래밍 언어는 null을 적절히 처리하는 방법이 없다. 그렇다면 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다. 즉, 인수로 null이 넘어오면 코드에 문제가 있다는 말이다.
시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 우리는 오픈소스, 패키지, 컴포넌트를 깔끔하게 통합해야 한다.
외부 패키지 테스트는 우리 책임이 아니다. 하지만, 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 간단한 테스트 케이스를 작성해 외부 코드를 익힐 수 있다. 이를 학습 테스트라고 한다. API를 사용하려는 목적에 초점을 맞춰서 진행한다. 패키지의 새 버전에도 대응이 가능하며, 이전이 쉬워 진다. 그렇지 않다면
새로운 클래스로 경계를 감싸거나 아니면 Adapter pattern을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. Typeorm이나 Prisma 처럼 말이다.
실제 코드와 맞먹을 정도로 방대한 테스트 코드는 심각한 관리 문제를 유발하기도 한다.
테스트 케이스가 있으면 변경이 두렵지 않다.
테스트당 하나의 개념을 테스트하며 assert문 개수는 최대한 줄여야 이해하기 쉽고, 어디서의 오류인지 빠르게 파악할 수 있다.
테스트 코드는 실제 코드만큼이나 중요하다. 테스트 API를 구현해 도메인 특화 언어를 만들자. 그러면 그만큼 테스트 코드를 짜기가 쉬워 진다.
클래스를 설계할 때도, 함수와 마찬가지로, '작게'가 기본 규칙이다.
멤버 변수나 상수는 static public final -> static private -> private 순서로 작성되어 내려오며, public 변수가 필요한 경우는 거의 없다. private method는 public method에서 호출된 직후에 기술된다.
캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.
하나의 책임을 진다. 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 말이다. Processor, Manager 등과 같이 모호한 단어가 있다면 클래스에다 여러 책임을 떠안겼다는 증거다.
응집도가 높다는 말은 논리적인 단위로 묶인다는 의미다. 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 여러 개로 쪼개준다.
클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다.
API를 직접 호출하는 대신 인터페이스를 생성한 후 메서드 하나를 선언하면, 클래스를 흉내내는 테스트용 클래스도 만들 수 있다. 이렇게 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다. 변경으로부터 잘 격리되어 있다는 의미다.
결합도를 최소로 줄이면 나오는 설계 원칙이다. 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.
프로그램 제작은 사용과 아주 다르다는 사실을 명심한다.
실제로 필요할 때까지 객체를 생성하지 않으면, 불필요한 부하가 걸리지 않는다.
public Service getService() {
if (service == null) {
service = new MyServiceImpl(...); // 인수에 의존하지 않을때만 가능
}
return service;
}
때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 생긴다. 이 때 사용하는 패턴.
인터페이스에 의존하는 기법이며, IoC(Inversion of Control)기법을 의존성 관리에 적용한 메커니즘이다. 프레임워크는 DI의 인터페이스 기반이다. 스프링 프레임워크를 예로 들면, 자바 DI 컨테이너를 제공하며, 객체 사이 의존성은 XML 파일에 정의한다. 대다수의 DI 컨테이너는 필요할 때까지는 객체를 생성하지 않고, 대부분은 계산 지연이나 비슷한 최적화에 쓸 수 있도록 팩토리를 호출하거나 프록시를 생성하는 방법을 제공한다.
개별 객체나 클래스에서 메서드 호출을 감싸서 대리로 수행하는 경우를 뜻한다. JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다. Has-a 관계가 Proxy API 기반이라고 보면 된다. Spring AOP, JBoss AOP등과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용한다.
POJO는 순수하게 도메인에 초점을 맞춘다. 프레임워크에 의존하지 않는 독립적인 객체라고 보면 된다. 학교 수업에서 배운 그 객체다. 따라서 테스트가 개념적으로 더 쉽고 간단하다.
프레임워크는 사용자가 모르게 프록시나 바이트코드 라이브러리를 사용해 영속성, 트랜잭션, 보안, 캐시, 장애조치 등과 같은 설정을 구현한다. 주요 객체를 생성하고 서로 연결하는 등 DI 컨테이너의 구체적인 동작또한 제어한다.
모든 정보가 애너테이션 속에 있으므로 코드 자체가 깔끔하고 깨끗하다. 즉, 그만큼 코드를 테스트하고 개선하고 보수하기 쉬워졌다. 이것도 자동화와 재사용의 일종이다.
관심사를 관점으로 분리하는 가장 강력한 도구는 AspectJ언어다. AspectJ는 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다. 스프링 프레임워크는 AspectJ에 미숙한 팀들이 애너테이션 기반 관점을 쉽게 사용하도록 다양한 기능을 제공한다.
관심사를 분리하는 방식은 그 위력이 막강하다. 도메인 논리를 POJO로 작성하여, 코드 수준에서 아키텍처 관심사를 분리할 수 있다면, 진정한 테스트 주도 아키텍처 구축이 가능해지고, 단순한 아키텍처를 복잡한 아키텍처로 키워갈 수도 있다.
아주 단순하하면서도 멋지게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 결과물을 재빨리 출시한 후, 기반 구조를 추가하며 조금씩 확장해나가도 좋다. 아무 방향 없이 프로젝트에 뛰어들라는 소리는 아니다. 범위, 목표, 일정은 물론이고 결과로 내놓을 시스템의 일반적인 구조도 생각해야 한다. 변하는 환경에 대처해 진로를 변경할 능력도 반드시 유지해야 한다.
표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다. 하지만 때로는 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못하거나 원래 표준을 제정한 목적을 잊어버리기도 한다. 그러니, 공식문서와 친해져라. 바로 만들지 않고 검색으로 찾아보고 확실하게 작업하라. 라이브러리를 쓸 때도 마찬가지다.
DSL은 간단한 스크립트 언어나 표준 언어로 구현한 API를 가리킨다. 좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 의사소통 간극을 줄여준다. 애자일 기법이 팀과 프로젝트 이해관계자 사이에 의사소통 간극을 줄여주듯이 말이다. DSL을 사용하면 고차원 정책에서 저차원 세부사항에 이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다.
모든 테스트를 실행하라.
중복을 없애라.
의도를 표현하라.
클래스와 메서드의 수를 최소로 줄여라.
검증이 불가능한 시스템은 절대 출시하면 안 된다. SRP를 준수하면 테스트가 쉽고, 결합도가 높으면 테스트 케이스를 작성하기 어렵다. DIP와 같은 원칙을 적용하고 DI, 인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮춰라.
코드를 점진적으로 리팩토링 해나간다. 시스템이 깨질까 걱정할 필요는 없다. 테스트 케이스가 있으니까. 응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 더 나은 이름을 선택하고, 중복을 제거하고, 의도를 표현하고, 클래스와 메서드 수를 최소로 줄여라.
작업을 처리하는 일부분을 서브 클래스로 캡슐화해 전체 기능은 그대로 두면서 특정 단계 내역을 바꾸는 패턴이다.
동시성은 책 하나를 할당할 정도로 복잡한 주제다.
동시성은 결합을 없애는 전략이다. '무엇'과 '언제'를 분리하는 전략이다. Servlet을 예로 들면, Servlet은 EJB Container에서 돌아가는데 이 컨테이너는 동시성을 부분적으로 관리해주어, 웹 요청이 들어오면 웹 서버는 비동기식으로 Servlet을 실행한다. 구조적 개선 뿐 아니라, 응답 시간과 작업 처리량 개선을 위해서 동시성 구현이 불가피하다.
동시성에 좋은 라이브러리들이 많다. 지속적으로 오픈소스를 공부하라. 아래는 동시성 설계 클래스 일부다.
코드가 올바르다고 증명하기에는 현실적으로 불가능하다. 그럼에도 충분한 테스트는 위험을 낮출 수 있다. 문제를 노출하는 테스트 케이스를 작성하고, 프로그램 설정과 시스템 설정, 그리고 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다.
프로그래밍은 과학보다 공예(Craft)에 가깝다.
한 방에 깨끗하고 우아한 프로그램이 나오지는 않는다. 프로그래밍은 공예에 가깝기 때문에 깨끗한 코드를 짜려면 지저분한 코드를 짠 뒤에 정리해야 한다. 그리고 개선이라는 이름 아래 구조를 크게 뒤집는 행위는 프로그램을 망치기 때문에, 개선 전과 똑같이 프로그램을 돌리면서 개선하는 게 좋으며, TDD기법을 사용해서 변경을 가한 후에도 변경 전과 똑같이 모든 테스트가 돌아가게 해야 한다. 변경 중간 중간의 과정에도 말이다.
리팩토링을 할 때에는 코드를 넣었다 뺐다 하는 사례가 아주 흔하다. 단계적으로 조금씩 변경하며 매번 테스트를 돌려야 하므로 여기저기 옮길 일이 많아진다. 큰 목표를 이루기 위해 자잘한 단계를 수없이 거친다.
소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다. 적절한 장소를 만들어 코드만 분리해도 설계가 좋아진다. 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다. 코드는 처음부터 깨끗하게 유지해야 하며, 아침에 엉망으로 만든 코드를 오후에 정리하기는 어렵지 않다.
코드를 리팩토링 하다 보면 원래 했던 변경을 되돌리는 경우가 흔하다. 리팩토링은 코드가 어느 수준에 이를 때까지 수많은 시행착오를 반복하는 작업이기 때문이다.
이전에는 접두어 f, I, ... 를 넣었지만, 지금은 IDE에서 잡아 주기 때문에 필요없다.
조건문은 캡슐화해서 표현해야 한다. if (shouldNotCompact())
부정문은 긍정문보다 이해하기 어렵기 때문에 if (canBeCompacted()) ... else
로 쓰는 게 더 좋다.
위상적으로 정렬하여 각 함수가 사용된 직후에 정의되도록 한다. 분석 함수가 먼저 나오고 조합 함수가 그 뒤를 이어서 나온다.
분석은 악의나 자만과는 거리가 멀다. 전문가 입장에서 수행하는 검토, 그 이상도 그 이하도 아니다. 우리 모두가 편안하게 여겨야 할 활동이다. 남이 내게 해준다면 감사히 반겨야 할 활동이다.
일반적으로 부모 클래스는 자식 클래스를 몰라야 바람직하다.
변수가 사용되는 위치에 가깝게 옮긴다.
테스트 커버리지는 높을 수록 좋으나, 너무 사소해 테스트할 필요도 없는 경우도 있다.
프로그램을 수정할 때마다 "왜?"라고 자문하라.
애플리케이션이 어디서 시간을 보내는지 알아야 한다.
프로세서 연산에 시간을 보내는 프로그램을 스레드를 늘린다고 빨라지지 않는다. CPU Cycle은 한계가 있기 때문이다.
주로 I/O 연산에 시간을 보낸다면 동시성이 성능을 높여주기도 한다.
서버가 만드는 스레드 수는 이론상 JVM(Java Virtual Machine)이 허용하는 수까지 가능하다. 공용 네트워크에 연결된 수많은 사용자를 지원하는 시스템이라면 시스템 동작이 멈출지도 모른다.
스레드를 관리하는 코드는 스레드만 관리해야 한다. 비동시성 문제까지 뒤섞지 않더라도 동시성 문제는 그 자체만으로도 추적하기 어렵다.
return ++lastIdUsed;
에 들어있는 바이트 코드 명령은 8개다. 만약 바이트 코드의 명령 단계가 N개이고 스레드가 T개라면 총 단계는 N*T개이며, 스레드 중 하나를 선택하는 Context Switch가 일어난다. 가능한 순열 수는 (NT)! / N!^T 가 된다.
만약 synchronized 키워드를 사용하면, 가능한 경우의 수는 T로 줄어즌다. 스레드가 2개일 때, A->B, B->A 두 경우만 존재하기 때문이다.
JVM 명세에 따르면 long 타입처럼 64비트 값을 할당하는 연산은 32비트 값을 할당하는 연산 두 개로 나눠진다. 연산 중간에 다른 스레드가 끼어들어 두 32비트 값 중 하나를 변경할 수 있다는 말이다.
현대 프로세서는 흔히 CAS(Compare and Swap)라 불리는 연산을 지원한다. DB에서의 Optimistic Locking이라는 개념과 유사하고 동기화 버전은 Pessimistic Locking이라는 개념과 유사하다.
락의 대가는 매우 비싸다. 그래서 현대에는 동시성을 위해 Atomic Operation을 지원하는 라이브러리들이 많이 생겨났다.
클라이언트 기반 잠금 메커니즘은 변경과 추적이 너무 어렵다. 일반적으로 서버-기반 잠금이 더 바람직하다. 이유는 아래와 같다.
서로가 서로의 자원을 필요로 하여 진행이 되지 않는다. 데드락은 아래 네 가지 조건을 모두 만족해야 한다.
여러 스레드가 한 자원을 공유하지만 동시에 사용하지 못하고 갯수가 제한적인 상황 아래는 예시다.
예방법은 아래와 같다.
일단 스레드가 자원을 점유하면 필요한 나머지 자원까지 모두 점유해 작업을 마칠 때까지 이미 점유한 자원을 내놓지 않는다.
예방법은 아래와 같다.
위 방식은 잠재적인 문제가 두 가지 있다. 아래와 같다.
기아는 CPU 효율을 저하시키는 반면 라이브락은 쓸데 없이 CPU만 많이 사용한다.
한 스레드가 다른 스레드로부터 자원을 빼앗지 못한다.
예방법은 아래와 같다.
T1이 R1을 점유하고 T2가 R2를 점유한다. 또한 T1은 R2가 필요하고 T2도 R2가 필요하다. 이 조건을 예방하는 것이 데드락을 방지하는 가장 흔한 전략이다.
예방법은 아래와 같다.
하지만 위 방식도 잠재적인 문제가 있다. 아래와 같다.
몬테 카를로 테스트. 임의로 값을 조율하면서 반복해 돌린다. 테스트가 실패한 조건은 신중하게 기록한다. 또한, 시스템을 배치할 플랫폼 전부에서 테스트를 돌린다. 실제 환경과 비슷하게 부하를 걸어 줄 수 있다면 그렇게 한다. 스레드 코드 테스트를 도와주는 ConTest같은 도구들을 적극 활용한다.