스트리트 코더(Street coder)
- Street coder
Street coder
거리로
요약
- 전문 소프트웨어 개발의 세계, 이 '거리'의 냉혹한 현실은 정규 교육에서 가르치지 않거나 우선하지 않는, 때로는 독학에서 완전히 놓치기 쉬운 일련의 기술을 요구한다.
- 새로운 소프트웨어 개발자들은 이론에 너무 신경 쓰거나 완전히 무시하는 경향이 있다. 결국 중간 어딘가를 찾게 되겠지만, 어떤 확실한 관점을 갖게 되면 이를 더 빠르게 얻을 수 있다.
- 최신 소프트웨어 개발은 20년 전보다 훨씬 더 복잡하다. 단순한 애플리케이션 하나를 개발하기 위해서도 다양한 수준의 엄청난 지식이 필요하다.
- 프로그래머는 소프트웨어를 직접 만들어 보는 것과 공부하는 것 사이의 딜레마에 직면한다. 좀 더 실용적인 방법으로 주제를 재구성하여 이를 극복할 수 있다.
- 작업할 내용에 대한 명확한 이해가 부족하면 프로그래밍이 일상적이고 지루한 작업이 되어 실제 생산성이 낮아진다. 하는 일을 더 잘 이해할수록 더 많은 기쁨을 얻게 될 것이다.
고찰
나는 내가 모르는 부분이 있을 때에 중요하지 않다 생각하면 간과하고 넘어가는 경향이 있다. 하지만 그럴 수록 나의 두려움은 커져간다. 작가는 '나의 불안함이 무능력보다 무지에서 비롯되었다'라고 한 뒤, 'PC 본체를 열고 내부를 보니 마음이 차분해졌다'고 한다.
어쩌면 내가 개발을 진행하며 느끼는 불안함 또한 그 무지에서 비롯된 것일지 모른다. 하긴, 내가하는 것을 모두 알고 있다면 그 불안함을 느끼겠는가? 어쩌면 작가의 좌우명처럼 나 또한 상자를 먼저 여는 것에 용기를 가져야 할지도 모른다. 대부분의 상자 속은 두려워하는 것보다는 덜 복잡하니 말이다.
무엇보다 그런 두려움은 일하는 시간을 줄일 수 있을 것이다. 소프트웨어 개발이 무엇인가? 사람이 하는 일을 더욱 편하게 해주는 것이 아닌가? 더욱 편하게 하는 것은 고객 뿐만이 아니다. 개발자도 마찬가지다. 내가 알고 있는 지식을 최대한 활용해서 일에 투자하는 시간을 줄이고 활용해라.
실용적인 이론
요약
- 컴퓨터 과학 이론은 지루할 수 있지만, 이론을 아는 것은 우리를 더 나은 개발자로 만들어준다.
- 타입은 일반적으로 강한 타이핑 언어에서 사용되는 것으로 알려져 있지만, 코드를 덜 작성하기 위해 사용될 수 있다.
- 타입을 사용하면 코드를 더 쉽게 설명할 수 있고, 주석을 덜 작성해도 된다.
- 값 타입과 참조 타입의 차이는 매우 크며, 값 타입을 알고 있으면 좀 더 효율적인 개발자가 될 수 있다.
- 문자열 내부가 어떻게 돌아가는지를 아는 것은 유용하며, 효율적인 개발에 도움이 된다.
- 배열은 빠르고 편리하지만 공개적으로 노출된 API에 가장 적합한 후보는 아닐 수 있다.
- 리스트는 동적으로 크기가 증가하는 경우에 유용하지만 그렇지 않을 경우 배열이 더 효율적이다.
- 연결 리스트는 독특한 데이터 구조이지만, 그 특성을 알면 딕셔너리 구조의 트레이드오프를 이해하는 데 도움이 된다.
- 딕셔너리는 빠른 키 검색에 유용하지만 GetHashCode()의 구현에 따라 성능이 크게 달라진다.
- 고유한 값을 갖는 리스트는 멋진 검색 성능을 위해 HashSet으로 표현할 수 있다.
- 스택은 특정한 단계를 추적하기 위한 훌륭한 데이터 구조다. 콜스택이 대표적이다.
- 콜스택이 작동하는 방식을 알면 값 타입이나 참조 타입이 성능에 미치는 영향을 보완할 수 있다.
고찰
최근 동료에게 감명깊은 이야기를 들었다. CS 지식은 그저 컴퓨터 이론을 뜻하는 것이 아니라, 내가 알고 있는 지식 중에 비어있는 지식을 채우는 용도라는 말이었다. 이 말을 듣고 요즘의 나는 Why라는 질문의 중요성을 잊고 문제를 쳐나가는 데에 급급했다는 것을 깨달았다. 그저 돌아가기만 하면 정답인 줄로 착각했다. 그러나 무심코 지나갔던 부분이 골칫거리로 전락하는 일이 발생하고 만다.
스스로에게 질문을 던지자. 내가 무엇을 알고 있다고 생각하는게 진짜 알고 있는 것이 맞는가? 꼬리에 꼬리를 무는 질문을 가져보자. 이런 측면에서 각 자료구조와 타입을 이해하는 건 필수라고 볼 수 있다. 이 책은 그런 필수 지식을 학습할 수 있는 정신적 틀을 제공한다.
문자열
- 텍스트 데이터를 의미하며, 사람이 읽을 수 있다.
Java에서는 String 클래스로 구현되고, 불변이다.
문자열 연결 (String Concatenation) 문제
왜 문제가 되는가?
String result = "";
for (int i = 0; i < n; i++) {
result = result + "데이터" + i;
}
- String의 불변성(Immutability):
- Java에서
String
객체는 불변(immutable)이다. 즉, 한번 생성되면 내용을 변경할 수 없다. - 각
String
객체는 내부적으로char[]
또는 Java 9부터는byte[]
배열에 문자 데이터를 저장한다.
- Java에서
- 힙 메모리 할당:
- 각 반복에서
result + "데이터" + i
연산 시 다음과 같은 일이 발생한다:- 현재
result
문자열의 내용을 읽는다. - "데이터" 문자열을 읽는다.
i
를 문자열로 변환한다.- 세 문자열을 합친 길이의 새로운
char[]
또는byte[]
배열을 힙에 할당한다. - 세 문자열의 내용을 새 배열에 복사한다.
- 새 배열을 참조하는 새
String
객체를 생성한다. - 새
String
객체의 참조를result
변수에 할당한다.
- 현재
- 각 반복에서
- 가비지 컬렉션 부담:
- 각 반복에서 이전
result
문자열은 더 이상 참조되지 않아 가비지 컬렉션 대상이 된다. - n번 반복하면 n개의 중간 문자열 객체가 생성되고 폐기된다.
- 각 반복에서 이전
- 시간 복잡도:
- i번째 반복에서 i 길이의 문자열을 복사해야 하므로 전체 시간 복잡도는 O(1+2+3+…+n) = O(n²)가 된다.
해결책
StringBuilder는 내부적으로 가변 배열을 사용하여 O(n) 시간 복잡도를 제공한다.
public String efficientConcatenation(int n) {
StringBuilder sb = new StringBuilder(n * 10); // 초기 용량 설정
for (int i = 0; i < n; i++) {
sb.append("데이터").append(i);
}
return sb.toString();
}
문자열 분할 (String Splitting) 문제
왜 문제가 되는가?
for (int i = 0; i < iterations; i++) {
String[] parts = text.split("\\s+");
}
- 정규식 패턴 컴파일:
split("\\s+")
호출마다 내부적으로Pattern.compile("\\s+")
작업이 수행된다.- 정규식 패턴 컴파일은 CPU 집약적인 작업으로, 다음 단계가 필요하다:
- 정규식 구문 파싱
- 상태 기계(State Machine) 생성
- 최적화 수행
- 컴파일된 패턴은
Pattern
객체로 힙 메모리에 저장된다.
- 메모리 할당:
- 매 반복마다 새
Pattern
객체가 힙에 할당된다. - 각
Pattern
객체는 내부적으로 복잡한 데이터 구조(상태 전이 테이블, 그룹 정보 등)를 포함한다. split
작업 결과로 새로운 문자열 배열(String[]
)이 할당된다.
- 매 반복마다 새
- CPU 캐시 비효율:
- 반복적인 패턴 컴파일은 CPU 캐시를 효율적으로 활용하지 못한다.
- 재사용 대신 매번 새로운 계산을 수행하므로 명령어 캐시와 데이터 캐시의 효율이 떨어진다.
해결책
import java.util.regex.Pattern;
public void efficientSplitting(String text, int iterations) {
Pattern pattern = Pattern.compile("\\s+"); // 패턴을 한 번만 컴파일
for (int i = 0; i < iterations; i++) {
String[] parts = pattern.split(text);
// 처리 로직
}
}
문자열 인턴 (String Interning) 활용
왜 문제가 되는가?
String s1 = new String("문자열");
String s2 = new String("문자열");
- 문자열 리터럴과 String 객체:
"문자열"
리터럴은 자바 클래스 로딩 시 문자열 상수 풀(String Constant Pool)에 저장된다.new String("문자열")
은 두 가지 객체를 생성한다:- 이미 상수 풀에 있는 "문자열" 리터럴
- 힙 메모리에 새로 할당되는
String
객체 (리터럴의 내용을 복사함)
- 메모리 중복:
- 동일한 내용의
String
객체가 여러 개 생성되면 각각 별도의 메모리 공간을 차지한다. - 애플리케이션에서 많은 중복 문자열이 있을 경우 상당한 메모리 낭비가 발생한다.
- 동일한 내용의
- String Constant Pool의 구조:
- JVM의 문자열 상수 풀은 Java 7부터 PermGen에서 힙 메모리로 이동했다.
- 내부적으로 해시 테이블 구조를 사용하여 문자열을 저장하고 조회한다.
intern()
메소드는 이 풀에서 동일한 내용의 문자열을 찾거나, 없으면 추가한다.
해결책
public void withInterning() {
String s1 = new String("문자열").intern();
String s2 = new String("문자열").intern();
System.out.println(s1 == s2); // true: 같은 객체 참조
}
대용량 문자열 처리: 메모리 맵 파일
왜 문제가 되는가?
String content = new String(Files.readAllBytes(Paths.get(filePath)));
- 물리적 메모리 제한:
readAllBytes()
는 파일 전체를 바이트 배열로 로드한다.- 큰 파일(수백 MB 이상)은 Java 힙의 상당 부분을 차지할 수 있다.
- 힙 메모리가 부족하면
OutOfMemoryError
가 발생한다.
- 배열 크기 제한:
- Java 배열은
Integer.MAX_VALUE
(약 2GB) 크기 제한이 있다. - 2GB 이상의 파일은 단일 배열로 로드할 수 없다.
- Java 배열은
- 가비지 컬렉션 압박:
- 대용량 문자열 객체는 가비지 컬렉션에 부담을 준다.
- 큰 객체는 Old Generation에 바로 할당되며, Full GC 빈도를 증가시킨다.
- Full GC는 애플리케이션 일시 중지(Stop-the-World)를 유발한다.
- 메모리 매핑의 원리:
- 메모리 맵 파일은 운영체제의 가상 메모리 시스템을 활용한다.
- 파일 데이터를 직접 메모리에 로드하지 않고, 필요할 때 페이지 단위(보통 4KB)로 로드한다.
- 메모리 맵은 프로세스의 가상 주소 공간을 사용하지만 물리적 메모리(RAM)는 실제 접근 시에만 사용한다.
해결책
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
public void efficientLargeFileProcessing(String filePath) throws IOException {
try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
FileChannel channel = file.getChannel()) {
long fileSize = channel.size();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
// 필요한 부분만 문자열로 변환하여 처리
byte[] bytes = new byte[1024]; // 청크 단위로 처리
while (buffer.hasRemaining()) {
int length = Math.min(buffer.remaining(), bytes.length);
buffer.get(bytes, 0, length);
String chunk = new String(bytes, 0, length, StandardCharsets.UTF_8);
// 청크 처리 로직
}
}
}
문자열 풀 및 캐싱 전략
왜 문제가 되는가?
public String getFormattedValue(int value) {
return "Value: " + value;
}
- 문자열 생성 비용:
- 매 메소드 호출마다 새로운 문자열 객체가 생성된다.
- 문자열 생성은 다음 단계를 포함한다:
- "Value: " 리터럴 접근
- value를 문자열로 변환
- 문자열 연결을 위한 StringBuilder 생성 (컴파일러 최적화)
- 결과 문자열을 위한 char[] 또는 byte[] 할당
- 새 String 객체 생성
- 캐시 라인과 지역성:
- 반복적인 동일 값 요청은 CPU 캐시 지역성(locality)을 활용하지 못한다.
- 캐시 미스(cache miss)가 증가하여 메모리 접근 지연 시간이 늘어난다.
- 캐싱의 원리:
ConcurrentHashMap
은 내부적으로 해시 테이블 배열과 노드를 사용한다.- 자주 사용되는 값의 문자열은 한 번만 생성되고 재사용된다.
- 동시성 제어를 위한 세그먼트 락(Java 7) 또는 CAS(Compare-And-Swap, Java 8+) 연산을 사용한다.
해결책
import java.util.concurrent.ConcurrentHashMap;
public class EfficientCache {
private final ConcurrentHashMap<Integer, String> cache = new ConcurrentHashMap<>();
public String getFormattedValue(int value) {
return cache.computeIfAbsent(value, k -> "Value: " + k);
}
}
문자열 비교 최적화
왜 문제가 되는가?
if (s.equals(target)) {
return true;
}
- equals() 메소드의 동작:
String.equals()
는 다음 단계로 작동한다:- 참조 동일성 검사 (s == target)
- 길이 비교 (s.length() == target.length())
- 각 문자를 한 글자씩 순회하며 비교
- 긴 문자열에서 마지막 글자만 다를 경우 거의 모든 글자를 비교해야 한다.
- CPU 파이프라인과 분기 예측:
- 문자열 비교는 조건부 분기를 유발한다.
- 문자 비교 루프는 CPU 파이프라인과 분기 예측기에 부담을 준다.
- 일치하지 않는 문자가 나올 때까지 계속 반복한다.
- 해시코드의 활용:
- 문자열의 해시코드는 내용을 기반으로 계산된다.
- 해시코드가 다르면 문자열이 다르다는 것이 확실하다(다른 내용이 같은 해시코드를 가질 수 있지만, 같은 내용이 다른 해시코드를 가질 수는 없다).
- 해시코드는 String 객체에 캐싱되므로 재계산되지 않는다.
- 메모리 접근 패턴:
- 최적화된 비교는 순차적인 메모리 접근을 줄여준다.
- 길이와 해시코드 비교는 단일 정수 비교로, 문자열 내용 전체를 검사하는 것보다 캐시 효율적이다.
해결책
public boolean efficientComparison(String[] array, String target) {
int targetLength = target.length();
int targetHash = target.hashCode();
for (String s : array) {
if (s.length() == targetLength && s.hashCode() == targetHash) {
if (s.equals(target)) { // 최종 검증 단계에서만 완전한 비교
return true;
}
}
}
return false;
}
배열
IEnumerable<T>
: 순차적으로 검토하는 경우
요소 읽기만 가능하며 수정은 불가능하다.
장점
- Lazy Evaluation : 필요할 때만 요소를 처리하므로 메모리 효율성이 높다.
- LINQ 호환성 : LINQ 쿼리와 완벽하게 호환된다.
- 단순성 : 단순한 순회만 필요한 경우 적합하다.
- 데이터 소스 추상화 : 실제 데이터가 어디에 있는지 상관없이 일관된 방식으로 접근 가능하다.
단점
- 제한된 기능 : 읽기만 가능하고 수정은 불가능하다.
- 성능 이슈 : 여러 번 순회하면 매번 처음부터 다시 계산해야 한다.
- Count 속성 없음 : 컬렉션 크기를 알려면 전체를 열거해야 한다.
ICollection<T>
: 반복적으로 액세스할 수 있는 카운트가 필요한 경우
요소를 추가, 제거, 검색할 수 있다.
장점
- 다양한 조작 : 요소 추가, 제거 등 다양한 컬렉션 조작이 가능하다.
- Count 속성 : 컬렉션 크기를 즉시 알 수 있다.
- 성능 : 요소 존재 여부 확인 등의 작업이 더 효율적이다.
단점
- 메모리 사용 : 모든 요소를 메모리에 로드해야 하므로 큰 데이터셋에는 부적합할 수 있다.
- 유연성 감소 : 구체적인 컬렉션 구현에 의존하게 된다.
리스트
가상 함수 호출
- 다형성은 객체의 인터페이스 변경 없이 기본 구현에 따라 객체가 동작할 수 있다.
- 할당된 객체 유형에 따라 초반에 호출할 가상 함수와 매핑한 테이블을 참조한다.
- 테이블을 가상 메서드 테이블(vtable)이라 부른다.
딕셔너리
스택
유용한 안티패턴
요약
- 논리적 종속성의 경계를 지켜 경직된 코드를 만들지 않도록 하라.
- 처음부터 다시 작성하는 것을 두려워하지 마라. 다음에 할 때는 이 일을 훨씬 더 빨리 할 수 있을 것이다.
- 앞길에 방해가 될 수 있는 종속성이 있다면 코드를 부수고 그것을 고쳐라.
- 코드를 최신 상태로 유지하고 정기적으로 발생하는 문제를 해결하여 스스로를 어려움에 처하지 않도록 하라.
- 논리적인 책임을 위반하지 않도록 코드를 재사용하는 대신 반복하라.
- 앞으로 코드 작성에 더 적은 시간이 걸리도록 스마트한 추상화를 개발하라. 추상화를 일종의 투자라고 생각하라.
- 사용하는 외부 라이브러리가 설계를 좌우하도록 내버려 두지 마라.
- 코드가 특정 계층에 얽매이지 않도록 하려면 상속보다는 합성을 선호하라.
- 위에서 아래로 읽기 쉬운 코드 스타일을 유지하도록 노력하라.
- 함수를 조기에 종료하고 불필요하게 if와 else를 사용하지 마라.
- 공통적인 코드를 한 곳에 두기 위해 goto 또는 로컬 함수를 사용하라.
- 나무와 숲을 구별할 수 없게 만드는 시시하고 중복적인 코드 주석을 피하라.
- 변수와 함수에 알맞는 이름을 지어 그 자체로 설명이 가능한 코드를 작성하라.
- 함수를 소화하기 쉬운 하위 함수로 쪼개 코드를 최대한 설명적으로 유지하라.
- 코드 주석이 유용할 때만 주석을 작성하라.
고찰
나는 지금까지 다양한 코드 형태를 겪었다. 그러다보니 위험을 탐지하는 감각도 얼추 생긴 듯 하다. 그럼에도 거대한 진흙공을 바라볼 때는 늘 막막한 감정에 휩싸이곤 하는데, 이 책에서는 빠르게 옮기고 깨버리라 말한다. 왜일까? 서로 얽혀있는 의존성이 코드의 경직성을 유발해 변화에 대한 저항성을 갖게 하기 때문이다.
또한, 테스트 코드의 중요성도 강조하는데 나는 이 부분에 매우 공감한다. 항상 버그가 발생하는 코드, 구체적으로는 기획된 내용이지만 개발자가 잘못하여 생긴 일은 대부분 테스트 코드가 부재했다. 나는 이런 상황을 다분하게도 목격하면서 테스트 코드의 중요성을 몸소 느낀다. 작가는 말한다. 코드를 깨버려 동작을 멈추게 하고, 위반 요소를 제거하고, 코드를 리팩터링하고, 그 영향을 처리하고, 의존하는 다른 부분을 처리하라. 그렇지만 테스트 코드가 제대로 작성되어있지 않다면 이런 일을 미루라고 말이다.
나는 코드를 깔끔하게 만들고 싶어 자주 시간을 내서 리팩토링을 진행한다. 다른 개발자들이 코드를 더욱 잘 읽히게 만들려는 의도가 크지만 나의 손이 더 능숙해지기를 바라는 마음도 있다. 그러나 가끔은 목적을 잃은 채로 그저 코드만 깔끔하게 만드는 경우도 있다. 항상 목적을 생각해야 한다는 점을 기억하자. 그리고 여전히 테스트는 중요하다.
맛있는 테스트
요약
- 애초에 테스트 작성을 많이 하지 않음으로써 테스트 작성에 대한 경멸을 극복할 수 있다.
- 테스트 주도 개발이나 그와 유사한 패러다임은 테스트 작성이 더욱 싫어지게 만들 수 있다. 내게 행복감을 주는 테스트를 작성하려고 노력하라.
- 특히 매개변수화된 데이터 기반 테스트와 함께 테스트 프레임워크를 이용하면 테스트 작성을 위한 수고를 크게 줄일 수 있다.
- 함수 입력의 경계 값을 잘 분석하면 테스트 수를 크게 줄일 수 있다.
- 데이터 타입을 적절하게 사용하면 불필요한 테스트를 많이 작성하지 않아도 된다.
- 테스트는 코드의 품질만을 보장하는 것이 아니라 여러분의 개발 기술과 처리량을 향상시키는 데도 도움을 줄 수 있다.
고찰
작가는 TDD와 같이 약어로 된 용어를 사용하지 말라고 하는데, 이 부분은 특히 작가와 나의 생각이 다르다. TDD에서 가장 원칙적으로 생각하는 것이 테스트가 실패하는 것을 확인하고 이 실패하는 테스트를 성공하게끔 만드는 것이다. 작가 본인은 실패하는 느낌을 준다하여 선호하지 않지만, 나는 오히려 이 사이클을 통해 내가 테스트를 할 것이 무엇인지 명확해지는 느낌을 받는다. 아마 개인의 성향 차이일 것이다. 여전히 방법론이기 때문이다. 하지만 테스트 작성과 프레임워크의 구문 요소, 테스트 구성에 시간을 많이 할애하는 점을 부정하는 건 나와 동일하다. 그렇기 때문에 대중적으로 잘 알려지고 많이 사용하는 JUnit으로 테스트 러닝 커브를 최소화하고 싶은 것이다.
여전히 나는 테스트 코드에 대한 갈증이 있다. 어떻게 하면 거대한 진흙공이 된 우리의 코드를 금빛이 나는 공으로 탈바꿈할 수 있을까? 고객에게는 어떻게 해야 피해가 가지 않도록, 가치를 의도한 대로 전달할 수 있도록 코드를 작성할 수 있을까? 이 모든 것을 챙기면서 일정을 지킬 수 있을까? 나는 항상 작업을 할 때 이런 생각들을 하는데, 여러 걱정 속에서 안정감을 찾도록 도와주는 것이 테스트였다. 테스트는 조급하지 않으면서 나를 안전하게 그 목적지로 데려다 줄 거라 확신한다. 커버리지가 전부가 아니다. 고객에게 제공되는 가치가 중요하다.
보람 있는 리팩터링
요약
- 표면에 드러난 것보다 더 많은 장점이 있으므로 리팩터링을 받아들여라.
- 점진적으로 대규모 아키텍처를 변경할 수 있다.
- 대규모 리팩터링 작업에서 발생할 수 있는 잠재적인 문제를 줄이기 위해 테스트를 사용하라.
- 비용뿐만 아니라 위험도 추정하라.
- 대규모 아키텍처를 변경할 때는 점진적인 작업을 위해 항상 마음 속에 혹은 어딘가 적힌 로드맵을 갖고 있어야 한다.
- 리팩터링할 때 밀접하게 연결된 종속성과 같은 장애물을 제거하기 위해 종속성 주입을 사용하라. 동일한 기술로 코드의 경직도를 줄이라.
- 득보다 실이 더 클 때는 리팩터링하지 않거나 뒤로 미루는 것을 고려하라.
고찰
리팩토링은 우리가 빠른 시간 동안 개발을 진행하면서 발생한 여러 범죄를 마주하게 하고, 스스로에게 질문을 던지도록 한다. 그것으로 끝나는 것이 아니라 어떻게 개선할지, 이 범죄를 회개하도록 도와준다.
아마 대부분의 리팩토링은 가벼운 방식이 아닌 꽤나 엄숙하고 진지한 방향일 것이다. '우리가 가야 할 방향은 이것이니, 이만큼 아름답게 만들 것이야.'와 같은 식으로 말이다. 물론 그런 포부는 좋지만, 한 번에 많은 변경 사항을 발생시키는 것은 결코 좋지 않다. 큰 변화는 예로부터 많은 버그와 여러 문제를 일으켜 왔으니 말이다.
우리는 나아가야 할 방향에 대한 로드맵을 정의하고 점진적으로 나아갈 수 있어야 한다. 우리는 리팩토링하려는 코드가 어떤 것과 연관있는지, 작업량은 얼마나 되는지, 위험도는 얼마나 되는지에 대한 판단을 기반으로 리팩토링을 진행해 나가야 한다. 이 모든 것을 파악할 수 있는게 바로 테스트 코드이다. 내가 이 코드를 리팩토링하는 이유가 '우아하게 만들기 위해서' 따위같은 것이 아니라면 이유가 있어야 할텐데, 테스트 코드가 없다면 그런 이유가 설명될 수 없다. 내가 하는 일을 모르게 될 확률이 크기 때문이다. 만약 내가 점진적이면서 효율적인 리팩토링을 원한다면 테스트 코드를 먼저 작성하자.
리팩토링의 목적
- 반복을 줄이고 코드 재사용을 증가시킨다.
- 정신 모델과 코드를 더 가깝게 한다.
- 코드를 더 이해하기 쉽고 유지관리하기 쉽도록 만든다.
- 특정 클래스에 버그가 발생하지 않도록 한다.
- 중요한 아키텍처 변화를 준비할 수 있다.
- 코드의 경직된 부분을 없앨 수 있다.
조사를 통한 보안
요약
- 보안 조치의 우선순위를 지정하고 약점을 파악하기 위해 혹은 종이에 적은 위협 모델을 사용하라.
- 중간에 보안을 다시 손보는 것은 어려울 수 있으니 보안을 처음부터 염두에 두고 설계하라.
- 숨기는 것에 의존하는 은둔 보안은 진짜 보안이 아니며, 심각한 손해의 원인이 될 수 있다. 우선순위를 정하라.
- 두 해시 값을 비교하는 경우에도 자신만의 보안 프리미티브를 직접 구현하지 마라. 이미 잘 테스트되고 잘 구현된 솔루션을 신뢰하라.
- 사용자 입력은 나쁘다.
- SQL 삽입 공격을 막기 위해 매개변수화된 쿼리를 사용하라. 매개변수화된 쿼리를 사용할 수 없을 경우, 사용자 입력을 적극적으로 검증하고 검사하라.
- XSS 취약성을 방지하려면 페이지에 포함된 사용자 입력이 HTML로 올바르게 인코딩되었는지 확인하라.
- DoS 공격을 방지하려면, 특회 성장하는 단계에서는 캡차를 피하라. 대신 스로틀링이나 적극적인 캐싱과 같은 다른 방법을 시도하라.
- 암호를 소스 코드가 아닌 별도의 암호 저장소에 저장하라.
- 목적에 맞게 설계된 강력한 알고리즘을 이용하여 암호 해시를 데이터베이스에 저장하라.
- 보안 관련 컨텍스트에서 GUID가 아닌 암호학적으로 안전한 의사 난수를 사용하라.
자기 주장이 뚜렷한 최적화
요약
- 이른 최적화를 연습하고 그것을 통해 학습하라.
- 불필요한 최적화로 스스로를 어려움에 빠뜨리지 마라.
- 항상 벤치마킹으로 최적화를 검증하라.
- 최적화와 대응성 사이의 균형을 유지하라.
- 중첩 루프, 문자열이 많은 코드, 비효율적인 불리언 표현식과 같이 문제가 있는 코드를 식별하는 습관을 가져라.
- 데이터 구조를 구축할 때는 더 나은 성능을 얻기 위해 메모리 정렬의 장점을 생각하라.
- 마이크로 최적화가 필요한 경우 CPU의 동작을 파악하고 캐시 지역성, 파이프라이닝, SIMD 같은 것을 다룰 수 있도록 하라.
- 올바른 버퍼링 메커니즘을 사용하려 I/O 성능을 향상시켜라.
- 스레드를 낭비하지 않고 코드와 I/O 작업을 병렬로 실행하기 위해 비동기식 프로그래밍을 사용하라.
- 비상시에는 캐시를 이용하라.
기분 좋은 확장성
요약
- 다단계 다이어트 프로그램처럼 점진적으로 확장성에 접근하라. 작은 개선을 통해 궁극적으로 더 나은 확장 가능한 시스템을 구축할 수 있다.
- 확장성의 가장 큰 블록 중 하나는 잠금이다. 이들과 함께 살 수 없고, 이들 없이는 살 수도 없다. 때로는 이것이 불필요하다는 것을 이해해야 한다.
- 코드 확장성 높이기 위해 수동으로 잠금을 획득하는 것보다 잠금이 없는 데이터 구조나 동시성 데이터 구조를 선호하라.
- 안전할 때는 항상 이중 점검 잠금을 사용하라.
- 더 나은 확장성을 위해 불일치를 활용하는 법을 배워라. 비즈니스에 적합한 수준의 불일치 타입을 선택하고 확장 가능한 코드를 만들 기회로 활용하라.
- ORM은 일반적으로 귀찮은 작업이지만, 여러분이 생각하지 못한 최적화로 확정성이 더 뛰어난 앱을 만들 수 있다.
- 뛰언나 확장성이 필요한 모든 I/O 바인딩 코드에 비동기 I/O를 사용하여 사용 가능한 스레드를 보존하고 CPU 사용량을 최적화하라.
- CPU 바운드 작업의 병렬화를 위해 멀티스레딩을 사용하라. 그러나 비동기식 프로그래밍 구조와 함께 멀티스레딩을 사용하는 경우라면 비동기식 I/O를 통한 확장성의 이점을 기대하지 마라.
- 마이크로서비스 아키텍처에 대한 설계 논의가 끝나기 전에 모놀리스 아키텍처는 전 세계 투어를 끝낼 것이다.
버그와의 동거
요약
- 중요하지 않은 버그를 수정하기 위해 리소스를 낭비하지 않으려면 버그에 우선순위를 정해야 한다.
- 해당 사례에 대해 계획적이고 의도적인 대응 방안이 있는 경우에만 예외를 포착하라. 그렇지 않으면 잡지 마라.
- 충돌을 사후에 방지하는 대신 먼저 충돌을 견딜 수 있는 예외 복원 코드를 작성하라.
- 오류가 일반적이거나 예상되는 경우에는 예외 대신 결과 반환 코드나 enum을 사용하라.
- 투박한 단계별 디버깅보다 더 빠르게 문제를 확인하기 위해 프레임워크에서 제공하는 추적 기능을 사용하라.
- 사용 가능한 다른 방법이 없을 경우 프로덕션에서 실행 중인 코드의 문제를 확인하기 위해 충돌 덤프 분석을 사용하라.
- 웹 사이트의 임시 작성글을 고무 오리 디버깅 도구로 사용하고 그동안 무엇을 시도했는지 자문해 보라.