아래 글은 로버트 마틴의 클린코드 책 14장 ~ 끝까지를 읽고 작성한 글입니다.
아래 내용은 저자가 코드를 짜면서 사용하는 기교와 휴리스틱이다. 앞단의 내용과 조금 겹치는 부분도 존재하고, 내용이 이어지기 때문에 앞의 내용을 다 읽어야 이해가 쉽다.
주석
- 부적절한 정보
작성자, 최종 수정일, SPR 번호 등과 같은 메타 정보만 주석으로 넣는다. - 쓸모 없는 주석
오래된 주석, 엉뚱한 주석, 잘못된 주석 등 쓸모 없는 주석은 코드를 그릇된 방향으로 이끈다. - 중복된 코드
코드만으로 충분한데 구구절절 설명하지 말자. - 성의 없는 주석
주석을 달 참이라면 시간을 들여 최대한 멋지게 작성한다. 단어를 신중하게 선택하고, 문법과 구두점을 올바로 사용하며, 간단하고 명료하게 작성하자. - 주석 처리된 코드
주석으로 처리된 코드를 내버려 두지 말고 지우라 !!
환경
- 빌드는 간단히 한 단계로 빌드해야 한다. 한 명령으로 전체를 체크아웃해서 한 명령으로 빌드할 수 있어야 한다.
(하나하나 타고 들어가서 빌드하는 것이 아니라 그냥 한 번에 처리할 수 있도록) - 모든 단위 테스트는 한 명령으로 돌려야 한다. IDE에서 버튼 하나로 모든 테스트를 돌린다면 가장 이상적이다. 모든 테스트를 한 번에 실행할 수 있도록 노력하자.
함수
- 함수에서 인수 개수는 작을수록 좋다.
- 출력인수는 직관을 정면으로 위배한다. 함수에서 무너가의 상태를 변경해야 한다면 출력인수보다는 함수가 속한 객체의 상태를 변경(입력으로 간주하여) 하자.
- 플래그 인수
Boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다 ! 되도록 피하자. - 죽은 함수
아무도 호출하지 않는 함수는 삭제한다.
일반
- 한 소스 파일에 한 언어만 사용하도록 하자.
- 당연한 동작 또한 구현하라!
최소 놀람의 원칙(The principle of Least Surprise)에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다. 당연한 동작을 구현하지 않으면 코드 작성자에 대한 신뢰도가 떨어지게 된다. - 경계를 올바로 처리하자.
모든 경계 조건, 구석진 곳, 기벽, 예외를 직관에 의존하지 말고 이 경계 조건을 테스트하는 테스트 케이스를 작성하라. - 안전 절차를 무시하지 말자.
- 중복되는 것들은 하나로 !
- 추상화로 개념을 분리할 때는 철저히 하자. 모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다.
- 기초 클래스는 파생 클래스를 아예 몰라야 한다.
- 과도한 정보를 제공하지 말자.
자료를 숨겨라. 유틸리티 함수를 숨겨라. 상수와 임시 변수를 숨겨라.
메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라. 하위클래스에서 필요하다는 이유로 protected 변수나 함수를 마구 생성하지 마라. 인터페이스를 매우 작게, 깐깐하게 만들어라. 정보를 제한해 결합도를 낮춰라 - 죽은 코드는 시스템에서 제거하라
- 변수와 함수는 사용되는 위치에 가깝게 정의하라.
- 일관성을 부여하라. 한 메서드를 processVertificationRequest라 명명했다면 유사한 요청을 처리하는 다른 메서드도 processDeletionRequest 처럼 유사한 이름을 사용한다.
- 비어 있는 기본 생성자, 아무도 사용하지 않는 변수, 호출하지 않는 함수 등은 깔끔하게 정리(삭제)하라.
- 서로 무관한 개념을 인위적으로 결합하지 말자. 예를 들어 일반적인 enum은 특정 클래스에 속할 이유가 없다. enum이 클래스에 속한다면 enum을 사용하는 코드가 특정 클래스를 알아야 한다. 뚜렷한 목적 없이 변수, 상수, 함수를 당장 편한 위치에 넣어버리지 말자.
- 기능 욕심을 부리지 말자. 메서드가 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다.
- static 을 부적절하게 사용하지 말자. 특히나 함수에서, 일반적으로 static 함수보다는 인스턴스 함수가 좋다. static 함수로 정의하겠다면 재정의할 가능성은 없는지 꼼꼼하게 따져보자.
- add(date) 보다는 addDaysTo(date), increaseByDays(date)라는 이름이 좋다. 이름과 기능이 일치하도록 함수명을 만들자.
- 의존하는 모든 정보를 명시적으로 요청하는 것을 보이라. 논리적인 의존성은 물리적으로 드러내라.
- If-Else문 혹은 Switch-Case문 보다는 다형성을 이용하라. switch문은 새 유형을 추가할 확률보다 새 함수를 추가할 확률이 높은 코드에서 더욱 적절하다.
- 표준 표기법을 따르라. 팀이 정한 표준은 팀원들 모두가 따라야 한다.
- 코드에서 무언가를 결정할 때에는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다.
호출하는 함수가 null을 반환할지도 모른다면 null을 반드시 점검한다.
조회 결과가 하나뿐이라고 짐작한다면 하나인지 확실히 확인한다.
통화를 다루어야 한다면 정수를 사용하고 반올림을 올바로 처리한다.
병행 특성으로 인해 동시에 갱신할 가능성이 있다면 적절한 잠금 매커니즘을 구현한다. - 조건을 캡슐화하라. if (timer.hasExpired() && !timer.isRecurrent()) 보다는, if (shouldBeDeleted(timer)) 가 더 낫다.
- 부정 조건은 피하라. 부정 조건은 긍정 조건보다 이해하기 어렵다. if (!buffer.shouldNotCompact()) 보다는, if (buffer.shouldCompact()) 가 더 낫다.
- 경계 조건을 캡슐화 (아래에서 nextLevel = level+1) 하라.
if (level + 1 < tags.length)
{
parts = new Parse(body, tags, level+1, offset+endTag);
body = null;
}
//보다는
int nextLevel = level + 1;
if (nextLevel < tags.length)
{
parts = new Parse(body, tags, level+1, offset+endTag);
body = null;
}
- 설정 정보는 최상위 단계에 둬라. 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘겨야지, 저차원 함수에 설정 관련 상수를 숨기지 말자.
자바
- 긴 import 목록을 피하고 와일드 카드를 사용하라.
import package.*; -> 이거 NO - 상수는 상속하지 않는다. 상수를 상속 계층 맨 위에 숨겨놓지 말고 그냥 static import를 사용하자.
- 상수와 enum(Java 5부터 사용 가능)을 적절히 사용하자.
이름
- 서술적인 이름의 함수명을 사용하자. 예) isStrike()
- 적절한 추상화 수준에서 이름을 선택하라.
누군가 연락을 취하는 과정에서의 함수가 있다고 하자. getconnectedPhoneNumber 과 같은 함수보다는 getConnectedLocator() 같은 것이 낫다. 연결 대상의 이름을 전화번호로 제한하지 않기 때문! - 가능하다면 표준 명명법을 사용하자. 예를 들어 DECORATOR 패턴을 활용한다면 장식하는 클래스 이름에 Decorator라는 이름을 쓰자. Impl 같은 것도 마찬가지
- 이름으로 부수 효과를 설명하라.
getOos() 보다는 createOrReturnOos 가 좋다. 함수에서 단순히 oos만 가져오는 것이 아니라 기존에 oos가 없으면 생성하기 때문
테스트
- 사소한 테스트를 건너뛰지 마라.
- 무시한 테스트는 모호함을 뜻한다.
- 경계 조건은 각별히 신경 써서 테스트한다.
- 버그 주변은 철저리 테스트하라.
- 실패 패턴을 살펴라. 때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 간단한 예로, 입력이 5자를 넘기는 테스트 케이스가 모두 실패한다면? 함수 둘째 인수로 음수를 넘기는 테스트 케이스가 실패한다면? 이런 곳에서 패턴을 생각할 수 있다.
- 테스트는 빨라야 한다.
동시성
앞에서 설명한 동시성을 좀 더 자세히 설명하는 챕터이다. 내가 현재 고민하고 있던 멀티스레딩 방식에 있어서, 가장 도움이 되었던 구절들과 부분들에 대해서만 간략하게 소개하려고 한다.
먼저 동시성과 관련해 동기와 비동기에 대해 추가적으로 공부한 내용을 아래 조금 첨언하였다.
동기와 비동기
동기와 비동기의 가장 큰 차이점은 ‘병렬적으로 처리할 수 있는가’로 설명할 수 있을 것이다. 동기 프로세스에서는 무조건 이전 단계가 완료되어야지만 이후 단계가 진행될 수 있다. 하지만 비동기(Async)에서는 여러 작업들이 병렬적으로 이루어질 수 있다.
다르게 표현한다면, 비동기(Asynchronous)라는 것은 쉽게 말해서 어떠한 작업이 완료되기를 기다리지 않고, 그 시간 동안 다른 작업을 하는 것을 의미한다.
1. 싱글 스레드 방식에서 다중 스레드 방식으로 바꾸면 성능이 높아질까?
이에 대한 해답은 우선 애플리케이션이 어디에서 시간을 보내는지에 대한 검토가 필요하다. 가능성은 두가지 이다.
1. I/O - 소켓 사용, DB 연결, 가상 메모리 스와핑 기다리기 등
2. 프로세서 - 수치 계산, 정규 표현식 처리, 가비지 컬렉션 등에 시간을 보낸다.
만약 프로그램이 주로 프로세서 연산에 시간을 보낸다면, 새로운 하드웨어를 추가해 성능을 높여 테스트를 통과하는 방식이 적합하지만, 주로 I/O 연산에 시간을 보낸다면 동시성이 성능을 높여주기도 한다.
2. Java에서 공유 자원 처리는 어떤 방식으로 수행할 수 있을까?
public class IdGenerator {
int lastIdUsed;
public int incrementvalue() {
return ++lastIdUsed;
}
}
---->
public Class Example{
int lastIdUsed;
public int resetId() {
lastIdUsed = 0;
}
public synchronized int getNextId() {
++lastIdUsed;
}
}
우선 위 처럼 확실히 원자적인 명령으로 나눈다.
상수 0으로 초기화하는 과정과, this.lastId를 0으로 할당하는 과정은 다른 스레드가 건드리지 못한다.
위의 예시와 같이 getNextId() 메서드를 synchronized로 선언하면 문제는 해결된다.
예를 들어 스레드 1과 스레드 2가 있다.
스레드 1이 lastIdUsed 를 42로 가져오는 과정에서 멈추었다. 이후 스레드 2가 끼어들어 처음부터 실행된다. 42에서 getNextId()를 가져가 43을 반환해 가져갔다. 이후 다시 스레드1이 실행되면 synchronized했던 작업을 덮어쓴다. 즉, 43+1이 아닌 42+1=43, 똑같은 값을 가져가게 된다.
결론적으로 동시성 코드를 파악할 때,
스레드가 서로의 작업을 덮어쓰는 과정에서,
1. 공유 객체/값이 있는 곳
2. 동시 읽기/수정 문제를 일으킬 소지가 있는 코드 (즉, 서로의 작업을 덮어쓸 수 있는 코드) -> 위에서는 getNextId()이다.
3. 동시성 문제를 방지하는 방법
을 파악하자.
2. 다중 스레드에서 안전하지 않은 클래스
- SimpleDataFormat
- DB 연결
- java.util 컨테이너 클래스
- 서블릿
+ 스레드 코드 테스트를 도와주는 도구로는 IBM의 ConTest 라는 도구가 있다.
'BackEnd' 카테고리의 다른 글
[크롤링|Lambda] AWS Lambda를 이용해 백준, 티스토리 크롤링하기 (2) | 2024.01.02 |
---|---|
[API] URL/URI 와 RESTFul API 설계 방법 (3) | 2023.12.20 |
[Java] 클린코드1 (7) | 2023.10.04 |
[후기] 객체지향의 사실과 오해 (4) | 2023.08.04 |
[MYSQL] MySQL - Advanced Class 수학 관련 함수 (0) | 2023.02.28 |