정규표현식

정규표현식이란?

정규 표현식은 패턴(pattern)으로 특정 목적을 위해 필요한 문자열 집합을 지정하기 위해 쓰이는 식이다.

메타문자

정규표현식에서 일정한 의미를 가지고 쓰는 특수문자를 메타 문자라고 부른다. 여기에 쓰이는 글자가 포함된 패턴을 넣으려면 해당 글자 앞에 \를 넣어줘야 한다.

  • ^ : 문자열의 시작
  • $ : 문자열의 종료. 옵션에 따라 문장의 끝 또는 문서의 끝에 매치된다.
  • . : 임의의 한 문자
  • []: 문자 클래스. 문자 클래스 안에 들어가 있는 문자는 그 바깥에서 하나의 문자로 취급된다.
    • ^ : 문자 클래스 내에서 ^는 not
    • - : ex) a-z는 a에서 z까지의 문자
  • | : or를 나타냄
  • ? : 앞 문자가 없거나 하나 있음
  • + : 앞 문자가 하나 이상임
  • * : 앞 문자가 0개 이상임
  • {n,m} : 앞 문자가 n개 이상 m개 이하. {0,1} 은 ?와 같은 의미다.
  • {n,} : 앞 문자가 n개 이상. 위의 형태에서 m이 생략된 형태이다. {0,} 이면 *와 같고 {1,} 이면 +와 같은 의미이다.
  • {n} : 앞 문자가 정확히 n개. {n,n} 과 같은 의미이다.
  • () : 하나의 패턴구분자 안에 서브 패턴을 지정해서 사용할 경우 괄호로 묶어주는 방식을 사용한다.
  • \s : 공백문자
  • \b : 문자와 공백 사이를 의미한다.
  • \d : 숫자 [0-9]와 같다.
  • \t : 탭문자
  • \w : 단어 영문자+숫자+_(밑줄) [0-9a-zA-Z_]
    문자 이스케이프는 대문자로 적으면 반대를 의미한다.

패턴 변경자

패턴구분자가 끝나면 그 뒤에 쓰는 것으로, 패턴에 일괄적으로 변경을 가할 때 사용한다. 정규식 엔진에 따라 변경자의 적용 방식이 상이하다.

  • i : 패턴을 대소문자 구분 없이 검사한다. 이 변경자를 사용할 경우 [a-z]로만 검사해도 자동으로 [a-zA-Z]와 같은 기능을 하게 된다. 영어가 아닌 언어(독일어, 프랑스어 등)를 다룰 때에는 버그 가능성이 있으므로 쓰지 않는 게 좋다. 대소문자라는 개념이 없는 한글, 한자, 가나문자는 이 패턴 변경자가 아무 역할도 하지 않는다.
  • s : 임의의 한 문자를 가리키는 . 메타 문자에 개행 문자(\n)도 포함시키도록 한다. 이 변경자를 사용하면 .이 줄바꿈도 임의의 한 문자로 취급하여 찾는다.
  • g : ^문자가 문장이 아닌 문서의 처음에, $ 문자가 문장의 끝(라인 피드 \n)이 아닌 주어진 문자열의 끝에 매치되게 변경한다.
  • m : 주어진 문자열에 줄바꿈이 있을 경우, 여러 줄로 취급하여 검사한다. (줄바꿈이 없다면 써도 의미가 없다.) 원래 정규표현식을 쓸 때 줄바꿈은 무시되는데, 이걸 사용하면 줄바꿈을 적용해서 검사한다. 그리고 ^은 한 줄의 시작, $는 한 줄의 끝으로 의미가 달라진다.
  • x : 공백 문자를 무시한다. 단, 이스케이프(역슬래쉬하고 같이 쓸 경우)하거나 문자 클래스 안에 있을 경우에는 예외. 정규식을 조금 더 읽기 편하게 만들어준다. 그러나 이 변경자를 지원하지 않는 엔진이 많은 게 단점이다.

Java

  • Java 경우에는 Pattern, Matcher 객체를 이용해서 원하는 문자열을 탐색할 수 있다.
  • String 객체의 replace도 정규식을 지원하는데 내부적으로는 Pattern, Matcher를 이용한다.
  • 특이하게도, Java에서는 Global 플래그가 디폴트로 적용되어 있다.
Pattern pattern = Pattern.compile("tomato", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher("tomato apple TOMATO");
while(matcher.find()) {
  System.out.println("count : " + matcher.groupCount());
  System.out.println("matched string : " + matcher.group());		// 패턴에 매칭된 문자열
  System.out.println("matched string : " + matcher.group(0));		// 패턴에 매칭된 문자열(matcher.group()과 동일)

}

- 결과 -
count : 0
matched string : tomato
matched string : tomato
count : 0
matched string : TOMATO
matched string : TOMATO

Javascript

  • Javascript 경우에는 RegExp와 String 객체에서 지원하는 함수를 이용해서 여러 정규식 처리를 할 수 있다.
  • RegExp 객체
    – exec() : 정규식 패턴과 일치하는 문자열을 배열로 반환한다. 없는 경우에는 null을 반환한다. 글로벌 플래그 사용시 lazy 하게 매칭된 문자열에 접근
    • test() : 정규식 패턴과 대응하는 문자열의 존재 여부를 true/false 로 반환한다.
    • match() : exec() 와 동일한 기능이지만, exec() 는 글로벌 플래그 사용시 즉시 매칭된 문자열 배열을 반환한다.
  • String 객체
    – search() : 정규식 패턴과 대응하는 문자열의 인덱스를 반환한다. 없는 경우에는 -1을 반환한다.
    • replace() : 정규식 패턴과 대응하는 문자열을 다른 문자열로 치환한다.
      – split() : 정규식 패턴을 기준으로 전체 문자열을 나눈다.
## exec() ##
let match;
const reg = /tomato/gi;
while((match = reg.exec('tomato apple TOMATO')) !== null) {
        // 매칭된 문자열 처리
}

- 결과 -
[ 'tomato',
  index: 0,
  input: 'tomato apple TOMATO',
  groups: undefined ]
[ 'TOMATO',
  index: 13,
  input: 'tomato apple TOMATO',
  groups: undefined ]

## test() ##
/tomato/gi.test('tomato apple TOMATO');

- 결과 -
true

## match() ##
"tomato apple TOMATO".match(/tomato/gi)

- 결과 -
[ 'tomato', 'TOMATO' ]

## replace() ##
"tomato apple TOMATO".replace(/tomato/gi, "banana")

- 결과 -
banana apple banana

## split() ##
"tomato apple TOMATO".split(/\s/)

- 결과 -
[ 'tomato', 'apple', 'TOMATO' ]

자주 쓰이는 패턴

1) 숫자만 : ^[0-9]*$

2) 영문자만 : ^[a-zA-Z]*$

3) 한글만 : ^[가-힣]*$

4) 영어 & 숫자만 : ^[a-zA-Z0-9]*$

5) E-Mail : ^[a-zA-Z0-9]+@[a-zA-Z0-9]+$

6) 휴대폰 : ^01(?:0|1|[6-9]) – (?:\d{3}|\d{4}) – \d{4}$

7) 일반전화 : ^\d{2,3} – \d{3,4} – \d{4}$

8) 주민등록번호 : \d{6} \- [1-4]\d{6}

9) IP 주소 : ([0-9]{1,3}) \. ([0-9]{1,3}) \. ([0-9]{1,3}) \. ([0-9]{1,3})

BigDecimal

조사하게 된 계기?

  • 할인 관련 API 작업을 진행중에 퍼센트 계산을 위해 나누기 연산을 하였는데이상한 오류를 발견했다. 분명 딱 떨어져야 하는 값인데! x.999999999로 나오고 있는!! ㅜㅜ 오픈 하루전에 찾아낸 버그라서.. 굉장히 마음 졸였었던 기억..

BigDecimal?

  • BigDecimal은 Java 언어에서 숫자를 정밀하게 저장하고 표현할 수 있는 유일한 방법이다.
  • 소수점을 저장할 수 있는 가장 크기가 큰 타입인 double은 소수점의 정밀도에 있어 한계가 있어 값이 유실될 수 있다.
  • Java 언어에서 돈과 소수점을 다룬다면 BigDecimal은 선택이 아니라 필수이다.
  • BigDecimal의 유일한 단점은 느린 속도와 기본 타입보다 조금 불편한 사용법 뿐이다.

double, 무엇이 문제인가?

double a = 10.0000;
double b = 3.0000;

// 기대값: 13
// 실제값: 13.000001999999999
a + b;

// 기대값: 7
// 실제값: 6.999999999999999
a - b;

// 기대값: 30
// 실제값: 30.000013000000997
a * b;

// 기대값: 3.33333...
// 실제값: 3.333332555555814
a / b;

BigDecimal 초기화

  • double 타입으로 부터 BigDecimal 타입을 초기화하는 방법으로 가장 안전한 것은 문자열의 형태로 생성자에 전달하여 초기화하는 것이다. double 타입의 값을 그대로 전달할 경우 앞서 사칙연산 결과에서 본 것과 같이 이진수의 근사치를 가지게 되어 예상과 다른 값을 얻을 수 있다.
  • https://stackoverflow.com/questions/7186204/bigdecimal-to-use-new-or-valueof/7186298#7186298
// double 타입을 그대로 초기화하면 기대값과 다른 값을 가진다.
// 0.01000000000000000020816681711721685132943093776702880859375
new BigDecimal(0.01);

// 문자열로 초기화하면 정상 인식
// 0.01
new BigDecimal("0.01");

// 위와 동일한 결과, double#toString을 이용하여 문자열로 초기화
// 0.01
BigDecimal.valueOf(0.01);

BigDecimal 비교 연산

  • BigDecimal은 기본 타입이 아닌 오브젝트이기 때문에 특히, 동등 비교 연산을 유의해야 한다. double 타입을 사용하던 습관대로 무의식적으로 == 기호를 사용하면 예기치 않은 연산 결과를 초래할 수 있다.
BigDecimal a = new BigDecimal("2.01");
BigDecimal b = new BigDecimal("2.010");

// 객체의 레퍼런스 주소에 대한 비교 연산자로 무의식적으로 값의 비교를 위해 사용하면 오동작
// false
a == b;

// 값의 비교를 위해 사용, 소수점 맨 끝의 0까지 완전히 값이 동일해야 true 반환
// false
a.equals(b);

// 값의 비교를 위해 사용, 소수점 맨 끝의 0을 무시하고 값이 동일하면 0, 적으면 -1, 많으면 1을 반환
// 0
a.compareTo(b);

BigDecimal 사칙 연산

  • Java에서 BigDecimal 타입의 사칙 연산 방법은 아래와 같다. 보다시피 double 타입보다 장황하고 귀찮은 편이다.
BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");

// 더하기
// 13
a.add(b);

// 빼기
// 7
a.subtract(b);

// 곱하기
// 30
a.multiply(b);

// 나누기
// 3.333333...
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
a.divide(b);

// 나누기
// 3.333
a.divide(b, 3, RoundingMode.HALF_EVEN);

// 나누기 후 나머지
// 전체 자리수를 34개로 제한
// 1
a.remainder(b, MathContext.DECIMAL128);

// 절대값
// 3
new BigDecimal("-3").abs();

// 두 수 중 최소값
// 3
a.min(b);

// 두 수 중 최대값
// 10
a.max(b);

BigDecimal 소수점 처리

  • RoundingMode.HALF_EVEN은 Java의 기본 반올림 정책으로 금융권에서 사용하는 Bankers Rounding와 동일한 알고리즘이다. 금융권에서는 시스템 개발시 혼란을 막기 위해 요구사항에 반올림 정책을 명확히 명시하여 개발한다.
// 소수점 이하를 절사한다.
// 1
new BigDecimal("1.1234567890").setScale(0, RoundingMode.FLOOR);

// 소수점 이하를 절사하고 1을 증가시킨다.
// 2
new BigDecimal("1.1234567890").setScale(0, RoundingMode.CEILING);
// 음수에서는 소수점 이하만 절사한다.
// -1
new BigDecimal("-1.1234567890").setScale(0, RoundingMode.CEILING);

// 소수점 자리수에서 오른쪽의 0 부분을 제거한 값을 반환한다.
// 0.9999
new BigDecimal("0.99990").stripTrailingZeros();

// 소수점 자리수를 재정의한다.
// 원래 소수점 자리수보다 작은 자리수의 소수점을 설정하면 예외가 발생한다.
// java.lang.ArithmeticException: Rounding necessary
new BigDecimal("0.1234").setScale(3);

// 반올림 정책을 명시하면 예외가 발생하지 않는다.
// 0.123
new BigDecimal("0.1234").setScale(3, RoundingMode.HALF_EVEN);

// 소수점을 남기지 않고 반올림한다.
// 0
new BigDecimal("0.1234").setScale(0, RoundingMode.HALF_EVEN);
// 1
new BigDecimal("0.9876").setScale(0, RoundingMode.HALF_EVEN);

BigDecimal 나누기 처리

BigDecimal b10 = new BigDecimal("10");
BigDecimal b3 = new BigDecimal("3");

// 나누기 결과가 무한으로 떨어지면 예외 발생
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
b10.divide(b3);

// 반올림 정책을 명시하면 예외가 발생하지 않음
// 3
b10.divide(b3, RoundingMode.HALF_EVEN);

// 반올림 자리값을 명시
// 3.333333
b10.divide(b3, 6, RoundingMode.HALF_EVEN);

// 3.333333333
b10.divide(b3, 9, RoundingMode.HALF_EVEN);

// 전체 자리수를 7개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333
b10.divide(b3, MathContext.DECIMAL32);

// 전체 자리수를 16개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333
b10.divide(b3, MathContext.DECIMAL64);

// 전체 자리수를 34개로 제한하고 HALF_EVEN 반올림을 적용한다.
// 3.333333333333333333333333333333333
b10.divide(b3, MathContext.DECIMAL128);

// 전체 자리수를 제한하지 않는다.
// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. 예외가 발생한다.
b10.divide(b3, MathContext.UNLIMITED);

BigDecimal과 Java Stream

// POJO 목록에서 BigDecimal 타입을 가진 특정 필드의 합을 반환
BigDecimal sumOfFoo = fooList.stream()
    .map(FooEntity::getFooBigDecimal)
    .filter(foo -> Objects.nonNull(foo))
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// 특정 BigDecimal 필드를 기준으로 오름차순 정렬된 리스트를 반환
foolist.stream()
    .sorted(Comparator.comparing(it -> it.getAmount()))
    .collect(Collectors.toList());

// 위와 동일한 기능, 정렬된 새로운 리스트를 반환하지 않고 원본 리스트를 바로 정렬
foolist.sort(Comparator.comparing(it -> it.getAmount()));

BigDecimal 유닛 테스트

  • BigDecimal은 JUnit에서 Assertion 기능을 제공하지 않아 유닛 테스트가 불편하다. AssertJ 라이브러리를 이용하면 아래와 같이 네이티브하게 BigDecimal에 대한 유닛 테스트를 수행할 수 있다.
  • https://joel-costigliola.github.io/assertj/
BigDecimal a = BigDecimal.valueOf(0.1);
BigDecimal b = BigDecimal.valueOf(0.10);
BigDecimal c = BigDecimal.valueOf(0.101);
BigDecimal d = BigDecimal.valueOf(0.001);

// equals()와 동일하기 때문에 소수점 마지막 0까지 동일해야 true
// false
assertThat(a).isEqualTo(b));

// compareTo()와 동일하기 때문에 소수점 마지막 0이 달라도 true
// true
assertThat(a).isEqualByComparingTo(b);

// 두 수가 주어진 오차 범위를 만족하면 true
// true
assertThat(a).isCloseTo(c, within(d));

BigDecimal 관련 라이브러리

  • big-math 라이브러리는 java.lang.Math 클래스의 BigDecimal 버전이라고 할 수 있다. BigDecimal 기반 연산과 관련된 여러 유용한 기능을 제공한다. 
  • https://github.com/eobermuhlner/big-math

SQL AntiPatterns 개발자가 알아야할 25가지 SQL 함정과 해법

논리적 데이터베이스 설계 안티패턴

무단횡단

  • 목표 : 관련된 값의 집합을 한 칼럼에 저장한다.
  • 안티패턴 : 칼럼을 VARCHAR로 만들고 여러개의 값을 쉼표로 구분하여 넣어서 DB 변경을 최소화 한다.
  • 해법 : 다대다 관계로 교차 테이블을 생성하라.
  • Tip : 각 값은 자신의 칼럼과 행에 저장하라.

순진한 트리

  • 목표 : 재귀적 관계를 갖는 데이터를 트리나 계층적 구조로 만든다.
  • 안티패턴 : parent_id 칼럼을 추가하라.
  • 해법 : 다른 트리 모델을 사용하라.
  • Tip : 계층 구조에는 항목과 관계가 있다. 작업에 맞도록 이 둘을 모두 모델링해야 한다.

아이디가 필요해

  • 목표 : 목표는 모든 테이블이 PK를 갖도록 하는 것이지만, PK의 본질을 혼동하면 안티패턴을 초래할 수 있다.
  • 안티패턴 : 모든 테이블에 id 칼럼 추가.
  • 해법 : 상황에 따라 다르다.
    • PK 칼럼의 이름을 “id”로 짓지 말라. bugs 테이블이라면 “bug_id” 형태로 지어라.
    • 가상키(surrogate key, auto increment나 sequence등을 통해 생성되는 키)에 집착할 필요는 없다. 자연키가 적합하면 자연키를 사용하라.
    • 복합키가 적합하면 복합키를 사용하라.
  • Tip : 관례는 도움이 될 때만 좋은 것이다.

키가 없는 엔트리

  • 목표 : 데이터베이스 아키텍처를 단순화하고 싶다.
  • 안티패턴 : FK 제약조건 생략.
    • 이 경우 참조 정합성을 보정하는 코드를 직접 작성하는 책임을 져야 한다.
    • FK를 사용하지 않으면 성능이 좋아질것 처럼 말하지만, FK를 사용하지 않으면 성능 문제를 포함해 문제가 해결되기보다는 늘어난다.
  • 해법 : 참조 정합성을 강제하는 FK를 사용하라. 데이터 정합성 오류를 찾아내 정정하는 대신, 처음부터 잘못된 데이터가 입력되지 않도록 하라.
  • Tip : 제약조건을 사용해 데이터베이스에서 실수를 방지하라.

엔티티-속성-값

  • 목표 : 가변 속성 지원(보통 엔티티의 상속 구조를 표현할 때 이런 경우가 발생한다)
    • 여러 객체의 계산이나 비교를 간단히 하기 위해 객체를 하나의 데이터베이스 테이블에 행으로 저장하고 싶다. 또한 객체의 각 서브타입이 베이스 타입이나 다른 서브타입에는 적용되지 않는 속성 칼럼을 저장하는 것도 허용해야 한다.
  • 안티패턴 : 범용 속성 테이블을 사용한다.
    • 이 말은, 키-값 쌍을 저장하는 테이블을 두는 것을 뜻한다. 값은 어떤 형태가 들어올지 알 수 없으므로 충분한 크기의 문자열로 지정한다.
  • 해법 : 서브타입 모델링 기법을 사용한다.
    • 단일 테이블 상속(Single Table Inheritance)
      • 관련된 모든 타입을 하나의 테이블에 저장하고, 각 타입에 있는 모든 속성을 별도의 칼럼으로 가지도록 한다.
      • 속성 하나는 행의 서브타입을 나타내는 용도로 사용해야 한다.
      • 많은 속성이 특정 서브 타입에만 필요하고, 해당 속성이 적용되지 않는 객체를 저장하는 경우 이런 칼럼에는 NULL을 넣어준다.
      • 서브 타입의 개수가 적고, 특정 서브타입에만 속하는 속성의 개수가 적을 때, 그리고 액티브 레코드와 같은 단입 테이블 데이터베이스 접근 패턴을 사용해야 할 때 가장 좋다.
    • 구체 테이블 상속(Concrete Table Inheritance)
      • 서브 타입별로 별도의 테이블을 만든다.
      • 각 테이블에는 베이스 타입에 있는 공통 속성뿐 아니라 특정 서브타입에만 필요한 속성도 포함된다.
      • 새로운 공통 속성이 추가되면 모든 서브 타입 테이블을 변경해야 한다.
      • 모든 서브타입을 한꺼번에 조회할 필요가 거의 없을 경우에 적합.
    • 클래스 테이블 상속(Class Table Inheritance)
      • 서브타입에 공통인 속성을 포함하는 베이스 타입을 위한 테이블을 하나 만든다.
      • 각 서브타입에 대한 테이블을 만든다.
      • 서브타입 테이블의 PK는 베이스타입 테이블에 대한 FK이다.
      • 모든 서브타입에 대한 조회가 많고 공통 칼럼을 참조하는 경우가 많을 때 적합하다.
    • 반구조적 데이터(Semistructured Data)
      • 단일 테이블 상속과 유사하지만 서브타입의 속성을 칼럼으로 저장하지 않는다.
      • 데이터의 속성 이름과 값을 XML또는 JSON 형식으로 부호화해 TEXT혹은 CLOB 칼럼으로 저장.
      • 서브타입의 개수를 제한할 수 없고, 어느 때고 새로운 속성을 정의 할 수 있는 완전한 유연성이 필요할 때 적합하다.
  • Tip : 메타데이터를 위해서는 메타데이터를 사용하라.

다형성 연관

  • 목표 : 여러 부모 참조
    • 댓글 엔티티가 있을 때, 이 댓글 엔티티가 Bugs 테이블과도 일대다 관계를 맺고, FeatureRequests 테이블과도 일대다 관계를 맺고 싶은 경우.
  • 안티패턴 : 이중 목적의 FK 사용. 이를 다형성 연관(Polymorphic Associations)라고 부른다. 한 컬럼이 다른 두개 이상의 테이블의 PK값을 저장하게 만든다. 따라서 해당 컬럼에는 FK 속성을 줄 수 없다.
  • 안티 패턴이 합당한 경우 : ORM을 사용할 경우 이 안티패턴 사용이 불가피 할 수 있다. 이 경우에는 ORM이 참조 정합성을 체크하므로 그나마 괜찮다.
  • 해법 : 관계 단순화
    • 역참조
      • 교차 테이블 생성 : 각 부모에 대해 별도의 교차 테이블을 생성하고 교차 테이블에는 각 부모 테이블에 대한 FK뿐 아니라 댓글에 대한 FK도 포함시킨다.
      • 신호등 설치 : 댓글은 항상 하나의 부모에 의해서만 참조 될 수 있으므로 교차 테이블에서 comment_id에 대해 unique 제약 조건을 건다.
    • 공통 수퍼테이블 생성
      • 모든 부모테이블이 상속할 베이스 테이블을 생성한다.
      • 댓글은 베이스테이블의 ID와 일대다 관계를 맺는다.
      • 각 부모테이블은 베이스 테이블로 부터 PK를 부여 받는다.
  • Tip : 모든 테이블 관계에는 참조하는 테이블 하나, 참조되는 테이블 하나가 있다.

다중 칼럼 속성

  • 목표 : 다중 값 속성 저장. 속성이 한 테이블에 들어가야 할 것처럼 보이는데, 여러개의 값을 가진다.
  • 안티패턴 : 여러개의 칼럼 생성. 아래 테이블에서 tag가 여러개 발생하는 경우.CREATE TABLE Bugs (bug_id SERIAL PRIMARY KEY,description VARCHAR(1000),tag1 VARCHAR(20),tag2 VARCHAR(20),tag3 VARCHAR(20));
  • 해법 : 다중 값 속성을 위한 칼럼을 하나 가지는 종속 테이블 생성. 여러 개의 값을 여러 개의 칼럼 대신 여러 개의 행에 저장하는 것이다.CREATE TABLE Tags (bug_id BIGINT UNSIGNED NOT NULL,tag VARCHAR(20),PRIMARY KEY (bug_id,tag),FOREIGN KEY (bug_id) REFERENCES Bugs (bug_id));
  • Tip : 같은 의미를 가지는 각각의 값은 하나의 테이블에 저장하라.

메타데이터 트리블

  • 목표 : 확장 적응성 지원. 쿼리 성능을 향상시키고 지속적으로 크기가 늘어나는 테이블을 지원하도록 데이터베이스를 구성하는 것이다.
  • 안티패턴 : 값으로 들어가야 할 것을 테이블 이름이나 컬럼 이름으로 만드는 행위
    • 많은 행을 가진 큰 테이블을 여러개의 작은 테이블로 분리한다. 작은 테이블의 이름은 테이블 속성 중 하나의 값을 기준으로 해서 짓는다.(예: Bugs_2009, Bugs_2010, Bugs_2011,..)
    • 하나의 칼럼을 여러 개의 칼럼으로 분리한다. 칼럼 이름은 다른 속성의 값을 기준으로 해서 짓는다.(예: bugs_fiexed_2008 INT, bugs_fixed_2009 INT, …)
  • 해법 : 파티션과 정규화
    • 테이블 수평 분할 : 행을 여러 파티션으로 분리하는 규칙과 함께 논리적 테이블을 하나 정의하면 나머지는 데이터베이스가 알아서 해준다.
      • 해시 파티션 : PARTITION BY HASH(YEAR(date_reported)) PARTITIONS 4
      • 시간별로 데이터를 분할하는 경우에는 range 파티션을 사용하는 것이 일반적이다. 이 경우 시간이 지남에 따라 새로운 파티션을 추가해야 한다.
    • 테이블 수직 분할 : 칼럼으로 테이블을 나눈다. BLOG, TEXT 칼럼 같은 것을 분리한다.
    • 칼럼의 경우에는 종속 테이블을 만들어서 해당 칼럼 값을 따로 저장한다.
  • Tip : 데이터가 메타데이터를 낳도록 하지 말라.

물리적 데이터베이스 설계 안티패턴

반올림 오류

  • 목표 : 정수가 아닌 수를 저장하고 이를 산술 연산에 사용한다. 산술 연산의 결과가 정확해야 한다.
  • 안티패턴 : FLOAT,REAL,DOUBLE PRECISION 데이터 타입 사용
    • 반올림 오류가 발생한다.
  • 해법 : NUMERIC이나 DECIMAL 타입을 사용하라.ALTER TABLE Bugs ADD COLUMN hours NUMERIC(9,2);
    • NUMERIC과 DECIMAL은 유리수가 반올림되지 않고 저장된다.
  • Tip : 가능하면 FLOAT을 사용하지 말라.
  • Precision and scale : http://sqlrelay.sourceforge.net/sqlrelay/programming/precisionscale.html
    • precision 정밀도 : 소수점 앞뒤를 모두 포함한 총 자리수
    • scale : 소수점 뒤 자리수
    • 예) 112.34 : precision 5, scale 2

31가지 맛

  • 목표 : 칼럼의 값을 고정된 집합의 값으로 제한하고, 해당 칼럼이 유효하지 않은 항목을 절대로 포함하지 않는다고 보장한다.
  • 안티패턴 : 칼럼 정의에 값지정
    • CHECK 제약조건 사용
    • MySQL에서 ENUM 사용
  • 안티 패턴 사용이 합당한 경우 : 값의 집합이 변할 가능성이 거의 없을 경우.
  • 해법 : 각 값을 행으로 저장하는 색인 테이블을 만들고, 해당 행을 FK 제약조건으로 참조하도록 한다.
  • Tip : 고정된 값의 집합에 대한 유효성 확인을 할 때는 메타데이터를 사용하라. 유동적인 값의 집합에 대한 유효성을 확인할 때는 데이터를 사용하라.

유령 파일

  • 목표 : 이미지 또는 이진 데이터 저장
  • 안티패턴 : 이미지를 파일 시스템에 파일로 저장하고 파일에 대한 경로만 VARCHAR로 데이터베이스에 저장한다.
  • 해법 : 아래에 대해 모두 답할 수 있게 애플리케이션을 설계했다면 파일을 사용하고, 그렇지 못하다면 BLOB을 사용하라.
    • 데이터의 백업과 복원 절차에서 데이터베이스와 이미지 파일 관계를 제대로 복원할 수 있는가?
    • 이미지가 계속쌓이는가? 필요하지 않으면 삭제되는가? 삭제는 자동인가 수작업인가?
    • 이미지에 대한 권한 검사가 필요한가? 권한이 없는 이미지를 사용자가 요청하면 뭘보게 되는가?
    • 이미지에 대한 변경을 취소할 수 있는가? 그렇다면, 애플리케이션이 이전 상태의 이미지로 복원해야 하는가?
  • Tip : 데이터베이스 밖의 리소스는 데이터베이스가 관리하지 않는다.

인덱스 샷건

  • 목표 : 성능 최적화. 데이터베이스의 성능을 향상시키는 가장 좋은 방법은 인덱스를 잘 활용하는 것이다.
  • 안티패턴 :
    • 인덱스를 불충분하게 정의하거나 또는 아예 정의하지 않는다.
    • 너무 많은 인덱스를 정의하거나 도움이 되지 않는 인덱스를 정의한다.
    • 어떤 인덱스도 도움이 될 수 없는 쿼리를 실행한다.
  • 해법 : 인덱스를 MENTOR하라. 인덱스 샷건은 적절한 이유 없이 인덱스를 생성하거나 삭제하는 것에 대한 안티패턴이다.
    • Measure 측정 : 느린 쿼리를 찾아라.
    • Explain 실행 계획 확인
    • Nominate 지명 : 쿼리 추적 통계 정보를 모으고 쿼리를 위해 있으면 좋은 새로운 인덱스 생성 권고 등의 변경 제안
    • Test 테스트 : 인덱스를 생성한 후, 쿼리를 다시 프로파일링해야 한다. 변경으로 인해 차이가 생겼고 작업이 제대로 됐다는 것을 확인하는 것이 중요하다.
    • Optimize 최적화 : 캐시 등.
    • Rebuild 재구성 : 주기적으로 인덱스 정비. MySQL에서 ANALYZE TABLE 또는 OPTIMIZE TABLE
    • 인덱스에 대한 맹목적 추측은 좋은 전략이 아니다.
  • Tip : 데이터를 알고, 쿼리를 알고, 인덱스를 MENTOR하라.

쿼리 안티패턴

모르는 것에 대한 두려움

  • 목표 : NULL을 포함하는 칼럼에 대한 쿼리 작성
  • 안티패턴 : NULL을 일반 값처럼 사용
  • 안티 패턴 사용이 합당한 경우 : NULL을 사용하는 것은 안티패턴인 아니다. NULL을 일반적인 값처럼 사용하거나 일반적인 값을 NULL처럼 사용하는 것이 안티패턴이다.
  • 해법 : 유일한 값으로 NULL을 사용하라.
    • NULL이 연관된 연산은 대부분 결과가 NULL임을 기억할 것
    • IS NULL/IS NOT NULL/IS DISTINCT FROM(피연산자가 NULL이더라도 true/false리턴, MySQL에서는 ⇔)
    • 어떤 칼럼이 논리적인 디폴트 값을 가지지 않더라도 NOT NULL 제약조건이 필요한 경우는 정당하고 흔하다.
    • COALESCE() 함수 : 가변인수를 받아서 NULL이 아닌 첫 인수 리턴
  • Tip : 어떤 데이터 타입에 대해서든 누락된 값을 뜻하는 데는 NULL을 사용하라.

애매한 그룹

  • 목표 : 그룹의 최댓값(또는 최솟값 또는 평균 값)뿐 아니라 해당 값을 찾은 행의 다른 속성도 포함하도록 쿼리를 작성하는 것이다.
  • 안티패턴 : group 되지 않은 칼럼을 참조한다.
  • 해법 : 단일 값 규칙(SELECT 목록에 있는 모든 칼럼은 그룹당 하나의 값을 가져야 한다). 여러가지 타계책은 책에.
  • Tip : 모호한 쿼리 결과를 피하기 위해 단일 값 규칙을 따라라.

임의(random)의 선택

  • 목표 : 임의(random)의 샘플 데이터만 리턴하는 효율적인 SQL 쿼리 작성
  • 안티패턴 : 데이터를 임의로 정렬해서 가져오기.
    • RAND()와 같은 비결정적 수식으로 정렬하면 인덱스를 활용할 수 없게 된다.
  • 안티 패턴 사용이 합당한 경우 : 목록 크기도 적당하고 데이터가 늘어날 일이 거의 없는 경우라면 괜찮다.
  • 해법 : 테이블 전체 정렬을 피하라.
    • 1과 PK 최대값 사이에서 임의의 값 선택. – 단, 1과 최댓값 사이에 빈 값이 없어야 함.
    • 1과 최대값 사이에서 임의의 값을 골라 그 다음으로 큰 값 사용. – 빈틈이 드물게 존재하고 모든 키 값이 동일한 빈도로 선택되는 것이 중요하지 않을 때.
    • 모든 키 값의 모록을 구하고서 임의로 하나 고르기. – 애플리케이션 코드 이용
    • 오프셋을 이용해 임의로 고르기. 오프셋 값을 랜덤으로 생성해서 LIMIT 1 OFFSET :offset으로
    • 벤더 종속적 방법
  • Tip : 어떤 쿼리는 최적화 할 수 없다. 이 경우에는 다른 접근방법을 취해야 한다.

가난한 자의 검색 엔진

  • 목표 : 전체 텍스트 검색
  • 안티패턴 : 패턴 매칭 사용
    • LIKE ‘%crash%’
    • REGEXP ‘crash’
    • 패턴 매칭 연산자의 주요 단점 : 성능이 안 좋다.
  • 안티 패턴 사용이 합당한 경우 : 필요할 때만 가끔 사용하는 쿼리
  • 해법 :
    • SQL(또는 관계형 이론)의 기본 원리 중 하나는 칼럼에 들어 있는 값이 원자적이어야 한다는 것이다. 즉, 한 값을 다른 값과 비교할 수 있지만, 비교를 할 때는 항상 전체 값과 비교해야 한다. SQL에서 부분문자열을 비교하는 것은 비효율적이거나 부정확하다.
    • DBMS가 제공하는 풀텍스트 인덱싱 기능
    • 써드파티 검색엔진 : Sphinx Search – DB 연동 가능, Apache Lucene
    • 인덱스 테이블 직접 만들기
  • Tip : 모든 문제를 SQL로 풀어야 하는 것은 아니다.

스파게티 쿼리

  • 목표 : SQL 프로그래머들이 일하면서 가장 흔하게 수렁에 빠지는 경우 중 하나가 “이걸 어떻게 하나의 쿼리로 할 수 있을까?”하고 생각할 때다.
  • 안티패턴 : 모든 결과를 하나의 쿼리로 만들려고 한다.
  • 해법 : 여러 쿼리로 나눠라.
    • 그냥 나눠.. 뭘 고민해.
    • UNION 연산 사용
    • SQL로 SQL을 자동 생성해서 실행
  • Tip : 하나의 SQL로 복잡한 문제를 풀 수 있을 것처럼 보이더라도, 확실치 못한 방법의 유혹에 넘어가면 안된다.

암묵적 칼럼

  • 목표 : 타이핑 줄이기.
    • * 기호는 모든 칼럼을 뜻한다. 따라서 칼럼 목록은 명시적이 아니라 암시적이다.
  • 안티패턴 : 칼럼 이름 지정 없이 와일드카드(*) 사용하기
  • 안티 패턴 사용이 합당한 경우 : 임시로 사용하고 버리는 SQL
  • 해법 : 와일드카드나 암묵적 칼럼 목록에 의지하기보다는, 항상 필요한 칼럼을 나열해야 한다.
  • Tip : 원하는 대로 가져가라. 그러나 가져간 건 다 먹어야 한다.

애플리케이션 개발 안티패턴

읽을 수 있는 패스워드

  • 목표 : 패스워드를 복구하거나 재설정하기
    • 패스워드를 평문으로 저장하는 것 뿐만 아니라 평문으로 네트워크(DB 접속 포함)로 전달하는 것도 안전하지 않다.
  • 안티패턴 : 패스워드를 평문으로 저장하거나 복호화 가능하게 저장하기
  • 안티 패턴 사용이 합당한 경우 : 패스워드로 다른 써드파티에 접속해야 할 필요가 있을 경우인데, 이때에도 평문이 아니라 복호화 가능한 방식으로 암호화하여 저장해야 한다.
  • 해법 : 패스워드의 소금(SALT) 친 해시 값을 저장한다.
    • 일방향 해시 함수를 사용해 패스워드를 부호화한다. 원래 문자열로 복호화 불가능한 알고리즘을 사용한다.
    • 권장 알고리즘 : SHA-256(SHA2),SHA-384,SHA-512 …
    • MD5,SHA-1 비권장
    • 단순히 패스워드만 해시하지 말고 각 사용자별로 따로 생성한 SALT를 더해서 해싱하라.
    • DB에 SQL 전송할 때 패스워드를 넣지 않도록 하라.(미리 해싱한 값을 전송)
    • 브라우저에서 웹서버로 패스워드를 보낼 때는 HTTPS 같은 보안 프로토콜 사용. → JavaScript로 RSA 암호화 로그인
    • 패스워드 복구 금지, 이메일로 패스워드 전송 금지.
  • Tip : 당신이 패스워드를 읽을 수 있으면, 해커도 읽을 수 있다.

SQL 인젝션

  • 목표 : 동적 SQL 쿼리 작성
  • 안티패턴 : 검증되지 않은 사용자의 입력을 그대로 SQL 쿼리로 날린다.
  • 안티 패턴 사용이 합당한 경우 : 이게 합당한 경우가 어딨냐?
  • 해법 :
    • SQL 인젝션은 파싱되기 전의 SQL문을 조작하는 방법으로 동작한다. SQL문이 파싱되기 전에 동적인 부분을 삽입하는 한, (직접 쿼리를 짜던, 프레임워크를 사용하던) SQL 인젝션 위험이 있는 것이다. ORM 프레임워크 사용한다고 해서 SQL 인젝션 위험이 사라지는게 아니다.
    • 입력 값 필터링
    • 쿼리 파라미터를 통한 값 전달(PreparedStatement와 ? 쿼리 파라미터)
      • 예외 : 쿼리파라미터로 값을 설정할 경우 쿼리 옵티마이저가 제대로 최적화를 못하는 경우가 있음. 이 때는 값을 검증해서 SQL문을 조합해 낼 것.
    • 요청 파라미터로 미리 정의된 값을 검색하게 하고 이 값을 SQL 쿼리에 사용하도록 한다. – Java에서는 enum으로 허용값을 정의하면 된다.
    • 동료와 코드 검토
  • Tip : 사용자가 값을 입력하게 하라. 그러나 코드를 입력하게 해서는 안 된다.

가상키 편집증

  • 목표 : 데이터의 ID값을 빠진 숫자 없이 정돈하자
  • 안티패턴 : ID값의 틈 메우기. 틈을 메우려 드는 순간 각종 문제가 터지기 시작한다.
  • 안티 패턴 사용이 합당한 경우 : 가상키 값을 바꿔야 할 이유는 없다.
  • 해법 : 가상키의 숫자를 정렬하려는 마음을 극복하라. PK 값은 유일하고 NULL이 아니어서 각 행을 참조하는 데 사용할 수 있어야 한다. 이게 전부다. 행을 식별하는 데 연속적인 숫자일 필요는 없다.
  • Tip : 가상키를 행의 식별자로 사용한다. 가상키는 행 번호가 아니다.

나쁜 것 안 보기

  • 목표 : 코드를 적게 작성하기
  • 안티패턴 : 데이터베이스 API 리턴 값 무시하는 코드. 애플리케이션 코드에 산재해 있는 SQL 코드의 단편만 보기
  • 안티 패턴 사용이 합당한 경우 : 객체지향언어에서 문제 발생시 예외를 던지므로 모든 문장에 대해 에러 여부를 고민하지 않아도 된다.(PHP 등의 일부 언어는 에러가 발생하면 예외를 던지지 않고 리턴 값에 예외 코드를 넣기 때문에 각각의 문장 실행후 예외 코드를 검사해야 하는 것으로 보임).
  • 해법 :
    • 에러 검사 코드 작성
    • 애플리케이션 코드에 중간중간 있는 SQL말고 실제 실행된 SQL 코드 확인
  • Tip : 코드의 문제를 해결하는 것만으로도 이미 충분히 어렵다. 보지 않고 작업해 스스로를 방해하지 말라.

외교적 면책특권

  • 목표 : 프로그램 개발의 관례 따르기. 대부분의 노련한 개발자들은 편의를 위해 이런 관례를 희생하는 것이 실패의 지름길임을 안다.
  • 안티패턴 : SQL은 프로그램이 아니라고 생각하고 일반적인 관례를 따르지 않는 경우.
  • 안티 패턴 사용이 합당한 경우 : 한 번 사용하고 바로 삭제해도 되는 임시 SQL를 만들 경우
  • 해법 : 초당적 품질 문화 확립. SQL도 일반적인 프로그래밍의 관례인 문서화, 버전관리, 자동화된 테스틀 하라.
  • Tip : 애플리케이션뿐만 아니라 데이터베이스에 대해서도 문서화, 테스트, 소스 코드 관리와 같은 소프트웨어 개발 관례를 사용하라.

마법의 콩

  • 목표 : MVC에서 모델 단순화하기
  • 안티패턴 : MVC 애플리케션에서 모든 모델 클래스가 액티브 레코드(ActiveRecord) 클래스를 상속하는 관행
  • 안티 패턴 사용이 합당한 경우 : 프로토타이핑
  • 해법 : 액티브 레코드를 가지는 모델
    • 모델과 액티브 레코드와 같은 DAO 사이의 관계는 IS-A(상속)가 아닌 HAS-A(집합연관)여야 한다. 액티브레코드를 사용하는 대부분의 프레임워크는 IS-A 관계를 가정한다.
  • Tip : 테이블에서 모델을 분리하라.

이 항목은 따로 설명하면, 액티브 레코드가 나쁘다는 얘기가 아니다.컨트롤러에서 DAO 혹은 액티브 레코드의 DB관련 메소드(혹은 그 외의 데이터 접근에 직접 관련된 메소드)를 직접 호출하지 말라는 얘기이다. Java로 설명하면 Controller는 Service를 호출하고 Service에서 DAO를 호출해야 한다. Controller에서 DAO를 직접 호출되면 안된다. 이 원칙을 액티브 레코드를 사용하는 MVC 프레임워크에서도 그대로 적용하며 Controller에서 ActiveRecord의 데이버테이스 접근 메소드를 직접 호출하지 말고 중간 모델 객체(서비스 객체)를 두라는 의미이다.

책 외의 다른 정보

EXIST()를 사용할 곳에 COUNT() 사용금지