Item49. 매개변수가 유효한지 검사하라

매개변수 검사 시점

method와 생성자 대부분은 입력 매개변수의 값이 특정 조건을 만족하기를 바란다. 예컨대 인덱스 값은 음수이면 안되며, 객체 참조는 null이 아니어야 한다는 식이다.

이런 제약은 반드시 문서화해야 하며, method 몸체가 시작되기 전에 검사해야 한다.

method 몸체가 실행되기 전에 매개변수를 확인한다면, 잘못된 값이 넘어왔을때 즉각적이고 깔끔한 방식으로 예외를 던질 수 있다.
그렇지 않은 경우에는 다음과 같은 단점을 가질 수 있다.

  1. method가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.
  2. 잘못된 결과가 나온다.
  3. 객체를 이상한 상태로 변경해서 , 해당 method에서는 정상적으로 수행되는 것 처럼 보이지만, 미래의 시점에서 해당 method와 관련없는 오류를 낸다.

매개변수 검사 관련 문서화

public 과 protected method는 매개변수 값이 잘못됬을 떄 던지는 예외를 문서화해야한다. (@throws javadoc 이용)
예를 들면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/***
* 현재값의 나머지 값을 반환한다.
* 이 method는 항상 음이 아닌
* @param m 계수 (양수여야 한다.)
* @return 현재값의 나머지 값
* @throws ArithmeticException m이 보다 작으면 발생한다.
*
*/
public BigInteger mod(BigInteger m){
if(m.signum() <= 0 ){
throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m);
}
//...
}
Read more

Item48. Stream 병렬화는 주의해서 적용하라

  • jdk 8 부터는 parallel method만 한번 호출하면 pipeline을 병렬 실행할수 있는 stream을 지원하기 시작했다.
  • stream 병렬화는 함부로 사용하면 오히려 성능 저하를 야기할 수 있다.

아래와 같아 처음 20개의 메르센 소수를 생성하는 프로그램이 있다고 가정하자

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
primes().map(p->TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes(){
return Stream.iterate(TWO ,BigInteger::nextProbablePrime);
}

이를 병렬로 처리하고자 stream pipeline의 .parallel() method를 호출하면 오히려 프로그램 수행 시간이 극단적으로 느려지게 된다.

그 이유는 stream library 가 pipeline을 병렬화하는 방법을 찾지 못했기 때문인데, 데이터 소스가 stream.iterate거나,중간 연산으로 limit를 쓰면 pipeline병렬화로는 성능개선을 기대할 수 없다.

limit을 다룰때 CPU 코어개수가 남는다면 원소를 몇 개 더 처리한 후 제한된 개수 이후의 결과를 버려도 아무런 해가 없다고 가정하는데, 위 메르센소수를 찾는 프로그램은 원소하나의 계산비용이 그 원소 이전에 계산한 모든 원소들의 비용을 합친 것 만큼 든다.

즉 메르센 소수를 찾는 프로그램의 경우 몇 개 더 처리한 후 제한된 개수 이후의 결과를 얻는 불필요한 비용 자체도 많이 든다.

Stream 병렬화 효과가 좋은 데이터

대체로 Stream의 Source가 ArrayList , HashMap , HashSet , ConcurrentHashMap 의 인스턴스거나 , 배열일때 병렬화의 효과가 가장 좋다.

위 병렬화에 적합한 자료구조들은 다음과 같은 특징을 가지고 있다.

  • 원하는 크기로 정확하게 손쉽게 나눌 수 있다. 즉 다수의 thread에게 일을 분배하기 좋다는 특징이 있다.

  • 참조 지역성(locality of reference)이 뛰어나다. 이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있다 (공간 지역성)
    만약 참조 지역성이 낮으면 Thread는 데이터가 메모리로 전송될떄까지 대기하게 된다.
    따라서 참조지역성은 다량의 데이터를 처리하는 벌크 연산을 병렬화할때 아주 중요한 요소로 작동하는데, 참조 지역성이 가장 뛰어난 자료구조는 기본 타입 배열이다.
    (참조 지역성이란 기억 장치(SSD,HDD)로부터 데이터가 참조될때, 시간적/공간적/순차적으로 분포가 집중되는 성질 )

Read more

제한적 직접 실행 (Limited Direct Execution)

제한적 직접 실행 (Limited Direct Execution)


  • 제한적 직접 실행과 반대되는 개념인 직접 실행(Direct Execution)은 프로그램을 한번 실행하면 종료될떄까지 cpu상에서 그냥 직접 실행시키는 것을 말합니다.

하지만 직접 실행 방식은 아래와 같은 문제점을 가지고 있습니다.

  1. 악의적인 process의 경우 os가 제어 불가
  2. 시분할 기법(time sharing) 구현 불가

+) 참고로 시분할 기법이란 process가 실행될떄 일정한 time quantuam 값내에서만 실행되고, 시간이 초과되면 timer interrupt를 발생시켜 다른 process로 context switching 하는 기법을 말합니다.

  1. 시스템 자원에 대한 제어 불가

예를 들면 user process가 디스크 입출력 요청이나, cpu , memory와 같은 시스템 자원에 추가할당을 요청할때, user process가 직접 시스템 자원을 꺼내쓴다면 악의적인 process의 경우 시스템 자원을 모두 점유하는 문제점이 있을 것입니다.

kernel mode , user mode


위 시스템 자원에 대한 제어권을 운영체제가 제어하기 위해서 나온 개념이 사용자 모드(user mode) , 커널 모드(kernel mode)입니다.

user process가 실행될떄는 user mode로 전환됩니다. 이 user mode에서는 할 수 있는 일이 제한되는데, 예를 들면 시스템 자원에 대한 요청 (ex)입출력 요청등) 이 제한됩니다.

시스템 자원에 대한 요청은 kernel mode에서 가능합니다. user mode 에서 kernel mode로 전환이 일어나는 메카니즘은 system call입니다.

Read more

Item46. Stream에서는 부작용 없는 함수를 사용하라

stream 은 api긴 하지만 함수형 프로그래밍에 기초한 패러다임이다.

stream 패러다임의 핵심은 계산 연산을 일련의 변환 단계들로 재구성하는 것이다. 각 변환 단계는 가능한 이전 단계의 결과를 받아서 처리하는 순수함수여야 한다.

  • 순수 함수 : 오직 입력만이 결과에 영향을 주고, 다른 가변 상태를 참조하지도 않는 함수

Item45. Stream은 주의해서 사용하라

Stream API 개념

Stream API는 Jdk 8부터 다량의 데이터 처리 작업 (순차적,병렬적) 을 도와주기 위해 추가되었다.

Stream API의 핵심 개념은 다음과 같다.

  • Stream : 유한 또는 무한개의 데이터 원소 시퀀스이다.
  • Stream pipeline : 원소들로부터 수행하는 각 연산단계를 표현하는 개념

Stream API 특징

  1. 데이터를 담고 있는 저장소가 아니다.
  2. Stream안의 데이터 원소들은 객체 참조나 기본 타입 값 (int,long,double 3가지 ) 이다
  3. Functional in nature. 원본 데이터를 변경하지 않는다.
  4. Laziness-seeking.
    stream API는 2가지 연산으로 나뉜다. map,filer 와 같은 intermediate operation (중개 연산) , collect와 같은 terminal operation (종료 연산)이 있다.
    중개 연산은 stream을 반환하고 종료 연산은 stream 타입을 반환하지 않는다. 중개 연산은 lazy evalutation(지연 평가) 된다고 공식 문서에 적혀 있는데,
    이는 종료형 연산( 예를 들면 .collect(toList())) 이 오기전까지는 중개 연산이 실행자체가 되지 않기 떄문이다.
1
2
3
4
5
List.of("a", "b", "c").stream().map((s) -> {
System.out.println("s = " + s);
return s.toUpperCase();
});
// terminal operation이 수행되기 전까지 실행되지 않음 (lazy)
  1. Stream으로 처리되는 데이터는 오직 한번만 처리된다 (각각의 stream 내 operator를 한번씩만 지나간다. )
  2. 스트림내 데이터 원소가 무제한일수도 있다. (short circuit method를 사용해서 제한할 수 있다. )
  3. 병렬처리가 가능하다.
1
2
3
4
5
6
7
8
// parallelStream : Jvm 이 병렬처리해줌 
// 데이터가 적은 경우에 불필요하게 병렬처리시에는 thread context swithing overhead 가 있어 오히려 느려지는 경우도 있음
foods.parallelStream().forEach((food)-> System.out.println("current thread : " + Thread.currentThread().getName()));
/*
current thread : ForkJoinPool.commonPool-worker-5
current thread : ForkJoinPool.commonPool-worker-19
current thread : main
*/

보다 자세한 stream에 대한 설명은 아래 reference 에서 확인할 수 있다.

(https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html)

Read more

Item44. 표준 함수형 인터페이스를 사용하라

표준 함수형 인터페이스

Java가 람다를 지원하면서 API를 작성하는 모범사례도 크게 바뀌었는데, 대표적으로 template pattern이 함수 객체를 받는 정적 팩토리 method나 생성자를 제공하는 형태로 변경되었다. 즉 함수 객체를 매개변수로 받는 생성자와 method를 더 많이 만들어야 하는데 , 이때 함수형 매개변수 타입을 올바르게 선택해야 한다.

예시로 LinkedHashMap의 removeEldestEntry method는 put method에 의해 호출되어 true를 반환하면 map에서 가장 오래된 원소를 제거한다.

1
2
3
protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
return size() > 100;
}

다음과 같이 100개 미만의 크기를 가진 map의 경우에는 원소를 제거하지 않고, 100개 이상부터 오래된 원소를 제거하도록 구현할 수 있는데, 이를 함수형 인터페이스로 변경가능하다.

주의할점은 위 method가 instance method이기 떄문에, 람다를 사용할떄에는 Map instance도 매개변수로 받아야 한다.

1
2
3
4
@FunctionalInterface
public interface EldestEntryRemovalFunction<K,V> {
boolean remove(Map<K,V> map , Map.Entry<K, V> eldest);
}

위처럼 직접 선언하는 것도 가능은 하지만 java 내장 library (java.util.function) 에 다양한 용도의 표준 함수형 인터페이스가 담겨 있다. 따라서 적절한게 있다면 직접 구현하지말고 표준 함수형 인터페이스를 사용하는게 좋다.

위의 직접 구현한 함수형 인터페이스인 EldestEntryRemovalFunction도 표준 함수형 인터페이스인 BiPredicate<Map<K,V> , Map.Entry<K,V>> 로 대체할 수 있다.

표준 함수형 인터페이스 종류

  • java.util.function에 총 43개의 함수형 인터페이스가 존재한다.
  • 대표적인 표준 함수형 인터페이스들은 다음과 같다.
Read more

정규화 (Normalization)

정규화 이론의 목표

  • 가능한 데이터 중복성(Redundancy)를 제거해서 한가지 사실은 한 곳에서만 나타난다라는 원칙을 지키도록 한다.
  • 즉 정규화 과정(Normalization procedure)란 중복을 최소화하기 위해 데이터를 구조화 하는 작업이다.
  • 정규화 과정을 통해 특정 조건을 만족시키는 relation을 정규형이라고 한다. 제 1,2,3…정규형 등이 존재한다.

왜 정규화가 필요한가?

  • 정규화를 하지 않는 경우에 data redundancy로 인해 다음과 같은 이상현상들이 발생할 수 있다.
  1. insert annomaly (삼입 이상 ) : 특정 데이터를 삼입하고 싶은데, 자료가 부족해 삼입할 수 없다. 예를 들면 공급자,도시,부품이라는 attribute가 있다고 하면 공급자가 어떤 도시에 살고 있다는 정보는 부품이라는 정보가 있어야만 삼입할 수 있다.

  2. deletion annomaly (삭제 이상) : 하나의 정보를 삭제하고 싶지만, 필요한 정보까지 삭제될 수 있다. 위 예와 동일한데, tuple을 삭제할 경우에 공급자가 어떤 도시에 살고 있다는 정보가 소실될 수 있다.

  3. update annomaly (갱신 이상) : 데이터 갱신 중간 과정에 일부 data는 update된 상태, 일부 data는 original 상태로 inconsistent한 상태가 생길 수 있다.

정규형 만족 조건

  • 분해 집합은 무손실 조인, 무손실 분해 (nonloss decomposition)를 만족해야 한다.
  • 분해 집합은 함수적 종속성(functional dependency)을 보존해야 한다.

Nonloss Decomposition (무손실 분해) 이란?

  • non-loss decomposition : 특정 relation 을 다른 relation으로 분해하는 것으로 , 이 과정은 정보의 손실이 있어서는 안된다. 즉 가역적이여야 한다.
  • 가역적이란 말은 분해 이후 다시 table을 join 하였을떄 최초의 relation과 동일해야 한다는 말이다.

Functional Dependency (함수적 종속성) 이란?

  • 특정 relation 안에서 하나의 속성 집합에서 다른 속성으로의 다대일 (many-to-one)관계이다. 정확한 정의는 다음과 같다.
R을 relation이라 하고, X와 Y를 R의 속성 집합의 임의의 부분집합이라고 할때, Y가 X에게 함수적으로 종속되기 위한 필요 충분 조건은 다음과 같다. R에 있는 각각의 X의 값이 정확하게 하나의 Y의 값과 관련을 갖는 것이다.

이를 “X가 Y를 함수적으로 결정한다.” 또는 기호로는 X->Y 로 표시한다.

Read more

Item43. 람다보다는 메소드 참조를 사용하라

method reference

아래는 임의의 키와 Integer값을 매핑하는 map 인데, 이떄의 merge method는 키,값,함수를 매개변수로 받아서 주어진 키가 맵안에 없다면 키,값쌍을 그대로 저장하고, 키가 이미 있다면 세번쨰 인수로 들어온 함수에 현재 값과 주어진 값을 적용해 키,함수의 결과쌍을 저장한다.

1
map.merge(key,1,(count,incr)->count+incr);

위에서 세번쨰 매개변수로 함수형 인터페이스를 구현한 람다는 그저 두 인수의 합을 반환하는 역할을 하는데 Integer의 static method인 sum을 이용하면 코드의 가독성을 증가시킬 수 있다.

1
map.merge(key,1,Integer::sum);

또한 람다로 구현했을때, 코드가 너무 길거나 복잡한 경우에 람다의 코드를 method로 추출해서 해당 method reference를 사용하는 방식이다.

이렇게 하게 되면 method에 적합한 naming을 할 수 있게 해준다.

Read more

Item42. 익명 클래스보다 람다를 사용하라

  • 함수 객체(function object) : 추상 method를 하나만 담은 interface 의 instance (JDK1.1이전)
  • 익명 class : Jdk 1.1이 등장하면서 함수 객체를 만드는데 사용됨.
1
2
3
4
5
6
7
// 익명 class 예시 
Collections.sort(words, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});

익명 class의 단점으로 추상 method를 하나만 구현하는데, 코드가 길어진다는 단점이 존재한다.

Lambda expression

  • Functional Interface : 추상 method 를 하나만 가지고 있는 interface
1
2
3
4
5
6
@FunctionalInterface
public interface FI {
void doSomething();
// default method는 있어도 상관없음
default void print(String str){ System.out.println("str = " + str); }
}
  • Lambda expression, Lambda : Function interface의 객체를 간결하게 구현할 수 있도록 지원해줌
  • 람다식을 이용하면 기존에 길었던 익명 class 코드를 줄일 수 있다. (paramter 의 type 추론도 지원해줌.)
1
Collections.sort(words,(s1,s2)->Integer.compare(s1.length(),s2.length()));

주의사항 : compiler가 람다식의 매개변수 타입을 추론할때, generic raw type 을 사용하면 추론이 불가능해짐으로, 꼭 타입매개변수를 적어주어야 함.

method reference 의 종류는 다음과 같다, 다음 Item 43에서 자세한 종류를 확인할 수 있다.

  • Static 메소드 레퍼런스
  • Instance 메소드 레퍼런스
  • Constructor 메소드 레퍼런스
Read more

연관관계 매핑

객체 연관관계와 테이블 연관관계의 차이점

  • 객체는 참조(주소)값을 기준으로 연관관계를 맺는다.(단방향)
    ex) X->Y , Y->X
  • 테이블은 외래키를 기준으로 연관관계를 맺는다. (JOIN operation , 양방향)
    ex) X JOIN Y 는 Y JOIN X 도 가능하다.

단방향 연관관계

  • 두 entity 중 어느 한쪽만 다른 한쪽을 참조하는 경우을 단방향이라고 한다.
Read more