표준 라이브러리를 사용하면 좋은점

선대 개발자들의 노고를 누리자

• 표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과, 앞서 라이브러리를 사용한 다른 개발자들의 경험을 활용할 수 있다.


쓸데없는데 시간을 허비하지 말자

• 핵심적인 일과 크게 관련없는 문제를 해결하느라 시간을 허비하지 않아도 된다.

• 자잘한 기능 개발이 아닌, 어플리케이션 핵심 기능 개발에 집중할 수 있다.


자동으로 개선되는 성능

 따로 노력하지 않아도 성능이 지속해서 개선된다.

 자바 플랫폼 라이브러리의 많은 부분이 수년에 걸쳐 다시 작성되며, 때로는 성능이 많이 개선되기도 한다.


기능 무한 증식

 라이브러리에 부족한 부분이 있다면 개발자 커뮤니티에서 이야기가 나오고, 논의된 후 다음 릴리스에 해당 기능이 추가되곤 한다.


알아보기 쉬운 코드 작성 가능

• 작성한 코드가 많은 사람에게 낯익은 코드가 된다.

    ◦  읽기 쉽고, 유지보수하기 좋고, 재사용하기 쉬운 코드


그럼에도 많은 개발자들이 기능을 직접 구현하는 이유

• 아마도 라이브러리에 그런 기능이 있는지 모르기 때문일 것이다.

• 메이저 릴리즈마다 주목할만한 수많은 기능이 라이브러리에 추가된다.

• 메이저 릴리즈마다 제공되는 릴리즈 노트에서 새로운 기능에 대한 설명을 참고하자.

 

JDK Release Notes

We’re sorry. We could not find a match for your search. We suggest you try the following to help find what you’re looking for: Check the spelling of your keyword search. Use synonyms for the keyword you typed, for example, try "application" instead of

www.oracle.com

 

표준 라이브러리 사용 예시

난수 생성 메소드

많은 개발자들이 메소드를 이렇게 작성하곤 한다.

• java.util.Random 을 이용하여 작성된 메소드 : 0 이상 n 미만의 수를 랜덤 반환한다.

•문제점

   ◦  n 이 그리 크지 않은 2의 제곱수라면 얼마 지나지 않아 같은 수열이 반복된다.

   ◦  n 이 2의 제곱수가 아니라면 몇몇 숫자가 평균적으로 더 자주 반환된다.

n 의 값이 크면 이 현상은 더 두드러진다.

   ◦  지정한 범위 바깥의 수가 종종 반환될 수 있다.

         -  rnd.nextInt( ) 의 반환값이 -2147483648 일 때

         -  자바의 int 타입은 32 bit 이므로 -2147483648 ~ 2147483647 의 값만 나타낼 수 있기 때문에
             rnd.nextInt() 이 -2147483648 를 반환하는 경우 절댓값 처리가 되지 않는다.

         -  Math.abs(-2147483648) = -2147483648

해결 방법

   ◦  java.util.Random 을 이용해 메소드를 작성하는 대신 Random.nextInt(int) 를 사용하면 된다.

         -  몇몇 숫자가 더 자주 반환되는 문제 해결

         -  범위 바깥의 수가 반환될 일도 없음

Java 7 부터는 성능이 훨씬 개선된 TreadLocalRandom 을 사용하는 것이 좋다.

Random.nextInt(int) vs TreadLocalRandom 성능 비교

포크-조인 풀이나 병렬 스트림에서는 SplittableRandom 을 사용하는 것이 좋다.


입력한 URL에서 내용을 가져오는 어플리케이션

자바 9에서 InputStream 에 추가된 transferTo 메서드를 사용하면 쉽게 구현할 수 있다.

Command Line 으로 입력 받기 까다로워 임시 String url 을 선언해 사용했다.

위 코드는 transferTo 를 이용해 이 기능을 완벽히 구현한 코드이다.

 

알아두면 좋은 라이브러리

java.lang

• Object 클래스

• String 클래스

• System 클래스

• Wrapper(래퍼) 클래스


java.util

• Arrays 클래스

• java.util.concurrent (동시성 라이브러리)

   ◦  멀티스레드 프로그래밍 작업을 단순화해주는 편의 기능 탑재

   ◦  자신만의 멀티스레딩 코드를 직접 구현할 수 있도록 도와주는 요소 탑재

   ◦  예) Semaphore 클래스, locks 패키지 등


java.io

• File 클래스

• BufferedReader / Writer 클래스

• Serializable 인터페이스


컬렉션 프레임워크

• List / Set / Map 클래스


스트림 라이브러리

• 기존 Java 에서 컬렉션 데이터를 처리할때는 for, for-each 루프문을 사용했음

   ◦  복잡한 처리 or 컬렉션의 크기가 커지면 성능 저하를 일으킴

• Stream : Java8에서 추가된 기능

   ◦  컬렉션 데이터를 선언형으로 쉽게 처리할 수 있음

   ◦  복잡한 루프문을 사용하지 않아도 되며, 루프문을 중첩해서 사용할 필요도 X

   ◦  병렬 처리를 별도의 멀티스레드 구현 없이 쉽게 사용 가능


Guava 라이브러리

• 구글이 작성한 자바 오픈 소스 라이브러리

•  Guava 사용 예시

Guava 를 이용해 리스트의 가장 마지막 원소 구하기

정리

어떤 기능이 필요할 때, 이미 구현된 라이브러리가 있는지 먼저 찾아보고 있으면 그걸 쓰자.

만약 찾아봤는데 없으면 구현해라. 즉, 무작정 구현부터 하진 말자. (그래야 너도 나도 좋다)

 

||  null 대신 빈 배열이나 빈 컬렉션을 반환하라.
||  null 을 반환한다고 해서 성능이 좋아지는 것도 아니고,
||  오히려 작성해야 하는 오류 처리 코드만 늘어나기 때문이다.

메서드에서 null 이 반환되면 생기는 일

private final List<Cheese> cheesesInStock = ... ;
public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
}
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.STILTON))
    System.out.println("good");

• getCheeses 메소드를 호출하는 쪽에서는 NullPointerException 을 피하기 위해 반환된 값이 null 인지의 여부를 매번

    체크해줘야 한다.

    ◦  이렇게 오류가 발생하는 것을 방지하기 위해 추가적으로 작성해주는 코드를 방어코드 라고 부른다.

    ◦  위 상황을 보면 불필요한 작업이 2개나 이루어지고 있다.

          -  getCheeses 메소드 : 반환하려는 대상의 Empty 여부를 체크하고 True 인 경우 null 반환

          -  getCheeses 메소드를 호출하는 쪽 : 메소드에서 반환된 값이 null 인지의 여부를 체크해 조건 처리

 

null 대신 빈 배열/컬렉션을 반환하는 경우

public List<Cheese> getCheeses() {
    return new ArrayList<>(cheesesInStock);
}

• cheesesInStock 에 값이 들어 있으면 그 값으로 ArrayList 가 구성되어 반환되고, 비어있으면 Empty ArrayList 가 반환된다.

    ◦ cheesesInStock 이 Empty 인지 확인하는 절차를 생략할 수 있다.

    ◦  당연히 getCheeses 메소드를 호출하는 쪽에서도 null 이 반환되지 않으니 null 을 처리하는 과정을 생략할 수 있다.

 

null 대신 빈 배열/컬렉션을 반환하는 것이 더 좋은 이유

• 빈 배열/컨테이너를 굳이 만들어서 반환하는데에도 비용이 발생하기 때문에 null 을 반환하는 것이 낫다는 주장도 있다.

• 하지만 아래 두가지 이유 때문에 이는 틀린 주장이라고 할 수 있다.

1) null 대신 빈 배열/컬렉션을 반환하는 경우의 성능 차이는 아주 미미하다.

• 분석 결과, 이 정도의 할당으로 인해 발생하는 성능 차이는 대부분 신경 쓸 수준이 되지 못한다.

2) 빈 배열/컬렉션은 굳이 새로 할당하지 않고도 반환할 수 있다.

• 만에 하나 빈 배열/컬렉션을 반환하는 행위가 성능 차이의 주범이 된다고 해도, 매번 새로운 배열/컬렉션을 할당하여 반환하는 대신

    빈 불변 배열/컬렉션을 하나 만들어두고, 매번 이 똑같은 객체를 반환 시킴으로써 간단하게 해결할 수 있다.

public List<Cheese> getCheeses() {
    return cheeseInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(cheeseInStock);
}

||  Collections.emptyList : 빈 불변 리스트를 반환하는 메소드
||  Collections.emptySet : 빈 불변 집합(Set)을 반환하는 메소드
||  Collections.emptyMap : 빈 불변 맵(Map)을 반환하는 메소드

 

• 단 이 역시 최적화에 해당하니 꼭 필요할 때에만 사용하고,

  사용한 경우에는 수정 전과 후의 성능을 측정하여 실제로 성능이 개선 되었는지 여부를 꼭 확인하자.


public Cheese[] getCheeses() {
    return cheesesInStock.toArray(new Cheese[0]);
}
// 또는 이렇게 작성할수도 있다.
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);

• 컬렉션 말고 배열을 사용하는 경우에는 위 코드처럼 구현할 수 있다.

    ◦ toArray 메소드에 파라미터로 넣어준 Cheese[0] 배열은 반환 타입을 지정해주는 역할

    ◦ 이 방식이 성능을 떨어뜨릴 것 같다면 이 역시 길이가 0인 배열(불변)을 미리 선언해두고 매번 그 배열을 사용하면 된다.

private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
    return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}

• 단, toArray 메소드에 넘기는 배열을 미리 할당해 두는 것이 오히려 성능 저하의 원인이 된다는 연구 결과도 있으므로,

    정말 필요한 상황이 아닌 단순히 성능을 개선할 목적이라면 이 방식은 지양하도록 하자.

메서드와 매개변수

• 메서드의 입력 매개변수 값에는 제약 조건이 있을 수 있다.

    ◦  ex) 인덱스 값은 음수이면 안된다.

    ◦  ex) 객체 참조는 null 이 아니어야 한다.

• 매개변수가 이러한 제약 조건을 위배하지 않는지는 메서드 body가 시작되기 전에 검사해주는 것이 좋다.

    ◦  오류는 가능한 한 빨리 잡아야 하기 때문

    ◦  오류를 발생한 즉시 잡지 못하면(문제가 생긴채로 어딘가에 저장되어 버리면) 해당 오류를 감지하기 어려워지고,

         감지하더라도 오류의 발생 지점을 찾기 어려워진다.


매개변수 검사를 하지 않으면 생길 수 있는 문제

• 메서드가 수행되는 중간에 모호한 예외를 던지며 실패할 수 있다.

• 메서드는 잘 수행되지만 잘못된 결과를 반환할 수 있다.

• 메서드는 잘 수행되지만 어떠한 객체의 상태를 변화시켜서 미래의 알 수 없는 시점에 이 메서드와 관련 없는 오류가 발생할 수 있다.


매개변수 관련 예외를 문서화 하는 방법

• 메서드의 매개변수 값이 잘못됐을 때 발생되는 예외를 문서화하면 개발자가 매개변수 관련 오류를 발생시킬 위험을 줄일 수 있다.

    ◦  @throws 자바독 태그를 사용할 수 있다.

    ◦  매개변수의 제약을 문서화 할 때는 제약을 어겼을 시 발생하는 예외도 함께 기술해주는 것이 좋다.

    ◦  일반적으로 발생하기 쉬운 예외

          -  IllegalArgumentException, IndexOutOfBoundsException, NullPointerException

/**
* (현재 값 mod m) 값을 반환한다. 
* 이 메서드는 항상 음이 아닌 BigInteger를 반환한다는 점에서 remainder 메서드와 다르다.
*
* @param m (계수 : 양수여야 한다)
* @return mod m (현재 값)
* @throws ArithmeticException (m 이 0 이하이면 발생한다)
*/

public BigInteger mod(BigInteger m) {
    if (m.signum() < 0) // m이 양수면 1, 0이면 0, 음수면 -1 반환
        throw new ArithmeticException("계수(m)는 양수여야 합니다. " + m);
    ...
}

• m == null 이면 m.signum 호출시 NullPointerException 예외가 발생한다.

    ◦  위 사항이 메서드 설명에서 언급되지 않는 이유

          -  이 설명은 메서드가 아닌, BigInteger 클래스 수준에 기술되어 있기 때문이다.

          -  모든 메서드에 일일이 주석을 작성해두는 것 보다, 클래스 단계에 주석을 한 번만 작성해두는 것이 훨씬 간편한 경우가 많다.


매개변수 검사에 사용할 수 있는 유용한 메소드

java.util.Objects.requireNonNull

this.value = Objects.requireNonNull(value, "예외메시지");

• java.util.Objects.requireNonNull 메서드를 활용하면 편리하게 null 검사를 수행할 수 있다.

    ◦  자바 7 에서 추가된 메서드이다.

    ◦  원하는 예외 메시지를 지정할 수도 있고, 입력을 그대로 반환하므로 값을 사용하는 동시에(언제든지) null 검사를 수행할 수 있다.

          -  반환되는 값을 사용하지 않고 오로지 null 검사만을 위해 사용해도 무방하다.

 

checkFromIndexSize , checkFromToIndex , checkIndex

public static int checkFromIndexSize(int fromIndex, int size, int length) {
  return Preconditions.checkFromIndexSize(fromIndex, size, length, null);
}

public static int checkFromToIndex(int fromIndex, int toIndex, int length) {
  return Preconditions.checkFromToIndex(fromIndex, toIndex, length, null);
}

public static int checkIndex(int index, int length) {
  return Preconditions.checkIndex(index, length, null);
}

• 자바 9 에서 추가된 메서드

• 리스트와 배열 전용으로 설계된 메소드이다.

• requireNonNull 과 달리 예외메시지를 지정할 수는 없다.

• 이상/이하(닫힌범위)는 다루지 못한다.

 

단언문(assert)

• public 이 아닌 메서드라면 단언문을 사용해 매개변수의 유효성을 검증할 수 있다.

    ◦  public 이 아닌 메서드의 경우, 직접 메서드가 호출되는 상황을 통제할 수 있다고 보는 것

    ◦  오직 유효한 값 만이 메서드에 넘겨지리라는 것을 보증할 수 있다.

private static void sort(long a[], int offset, int length) {
        assert a != null;
        assert offset >= 0 && offset <= a.length;
        assert length >= 0 && length <= a.length - offset;
        //계산 수행 ...
}

• 단언문은 자신이 단언한 조건이 무조건 참이라고 선언한다.

• 단언문과 일반적인 유효성 검사의 차이

    ◦  단언문은 실패하면 AssertionError 를 던진다.

    ◦  Runtime 효과/성능 저하를 전혀 발생시키지 않는다.


나중에 쓰려고 저장하는 매개변수의 유효성을 검사하라

• 메서드가 직접 사용하지는 않지만, 이후 다른 연산에서 사용하기 위해 저장되는 매개변수는 특히 더 신경써서 검사하는것이 좋다.

    ◦  생성자가 이 원칙의 한 예시이다.

    ◦  생성자 매개변수의 유효성 검사는 클래스 불변식을 어기는 객체의 생성을 방지하기 위해 반드시 필요


메서드 매개변수 유효성 검사 규칙의 예외

• 다음과 같은 경우에는 메서드 body 실행 전 매개변수의 유효성을 검사하지 않아도 된다.

    ◦  유효성 검사 비용이 지나치게 높거나 실용적이지 않은 경우

    ◦  계산 과정에서 암묵적으로 검사가 수행되는 경우

          -  ex) Collections.sort(List) : 객체 리스트를 정렬하는 메서드

          -  리스트 안의 객체들은 모두 상호 비교될수 있어야 하며, 정렬 과정에서 이 비교가 이루어진다.

          -  만약 상호 비교할 수 없는 타입의 객체가 들어 있다면 그 객체와 비교할 때 ClassCastException 이 발생한다.

    ◦  때로는 계산 과정에서 필요한 유효성 검사가 이루어지지만, 계산에 실패했을 때 의도한것과 다른 예외가 발생할 수도 있다.

          -  잘못된 매개변수 값을 사용해서 발생된 예외와, 문서에서 던지기로 작성된 예외가 다를 수 있다.

          -  이 경우 예외번역(exception translate) 관용구를 사용해 문서에 작성된 예외로 번역해줘야 함

try {
    ... // 저수준 추상화를 이용한다.
} catch (LowerLevelException e) { // 추상화 수준에 맞게 번역한다.
    throw new HigherLevelException(...);
}

아이템을 마치며

• 이번 아이템을 '매개변수에 제약을 두는 것이 좋다' 라고 해석해서는 안 된다.

• 메서드는 최대한 범용적으로 설계되어야 하며, 매개변수 제약은 적을수록 좋다.

자바에 람다가 등장하면서 생긴 변화

람다와 정적 팩토리 패턴,생성자

• 템플릿 메서드 패턴의 매력이 크게 줄어들었고, 대신 람다와 함께 함수 객체를 받는 정적 팩토리생성자 패턴을 사용하기 시작했다.

• 즉, 람다를 이용하여 함수 객체를 매개변수로 받는 생성자와 팩토리 메서드를 사용하는것이 요즘 트렌드이다.

• 이때 주의할 점은 함수형 매개변수 타입을 올바르게 선택해야 한다는 것이다.

 

• 템플릿 메서드 패턴이란?

   ◦  어떤 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체 일을 수행하는 구조는 바꾸지 않으면서 특정 단계에서 수행하는

        작업을 바꾸는 패턴. 즉, 다른 부분은 다 같은데 일부분만 다른 메서드가 여러개 있는 경우의 코드 중복을 최소화 시킬 때 유용하다.

        다른 관점에서 보면 동일한 기능을 상위 클래스에서 정의하면서 확장/변화가 필요한 부분만 서브 클래스에서 구현할 수 있도록 하는

        패턴이다. 예를 들어, 전체적인 알고리즘은 상위 클래스에서 구현하면서 다른 부분은 하위 클래스에서 구현할 수 있도록 함으로써

        전체적인 알고리즘 코드를 재사용하는 데 유용한 환경을 제공해준다.

 

• 정적 팩토리와 생성자 (Item1 : 생성자 대신 정적 팩토리 메소드를 고려하라)

public Test(String name) { this.name = name; } // 생성자
public static Test withName(String name) { return new Test(name); } // 정적 팩토리 메소드

Test test = new Test("Gyunny"); // 생성자 사용
Test test = withName("Gyunny"); // 정적 팩토리 메소드 사용

 

• 자바 클래스, 객체, 인스턴스

   ◦  클래스 : 객체를 만들어 내기 위한 설계도 혹은 틀. 연관되어 있는 변수와 메서드의 집합

   ◦  객체 : 클래스에 선언된 모양 그대로 생성된 실체

         -  클래스의 인스턴스(instance) 라고 부르기도 한다.

         -  객체는 모든 인스턴스를 대표하는 포괄적인 의미를 갖는다.

         -  OOP의 관점에서 클래스의 타입으로 선언되었을 때 객체 라고 부른다.

   ◦  인스턴스 : OOP 관점에서 객체가 메모리에 할당되어 실제 사용될 때 인스턴스 라고 부른다.

         -  객체는 클래스의 인스턴스다.

         -  객체 간의 링크는 클래스 간의 연관 관계의 인스턴스다.

         -  실행 프로세스는 프로그램의 인스턴스다.

         -  인스턴스라는 용어는 반드시 클래스와 객체 사이의 관계로 한정지어서 사용할 필요는 없다.

         -  인스턴스는 어떤 원본(추상적인 개념)으로부터 ‘생성된 복제본’을 의미한다.

 

• LinkedHashMap을 예시로 생각해보자. 이 클래스의 removeEldestEntry 메서드를 재정의하면 캐시처럼 사용할 수 있다.

public class CacheExample {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<String, Integer>() {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) { return size() > 3; } // 가장 최근 데이터 3개만 유지
        };

        map.put("a", 1); map.put("b", 2);
        map.put("c", 3); map.put("d", 4); // 가장 최근 데이터 3개 : b,c,d 만 유지된다.

        System.out.println(map); // { b=2, c=3, d=4 }
    }
}

   ◦  잘 동작하지만 람다를 이용하면 함수 객체를 받는 정적 팩터리나 생성자를 이용했을 것이다.

 

람다와 함수형 인터페이스

• 위 코드에서 재정의한 removeEldestEntry는 size메서드를 호출하는데, 매개변수의 인스턴스 메서드라 가능하다.

• 하지만 팩터리 메소드나 생성자를 호출할 때는 Map의 인스턴스가 존재하지 않기 때문에 Map 자체도 함수의 인자로 넘겨줘야 한다.

• 이를 함수형 인터페이스로 선언하면 아래와 같다.

@FunctionInterface interface EldestEntryRemovalFunction<K, V> {
    boolean remove(Map<K,V> map, Map.Entry<K, V> eldest);
}

• 람다 표현식으로 구현 가능한 인터페이스 = 추상 메서드가 1개인 인터페이스 = 함수형 인터페이스

• java.util.function 패키지에서 제공하는 함수형 인터페이스 = 표준 함수형 인터페이스

    ◦  즉, 위 코드처럼 직접 구현할 필요 없이 이미 구현되어 있는 표준 함수형 인터페이스를 가져다 쓰면 된다.


표준 함수형 인터페이스

• 필요한 용도에 맞는게 있다면, 직접 구현하는 것보다 표준 함수형 인터페이스를 사용하는 것이 좋다.

• 관리할 대상도 줄어들고 제공되는 많은 유용한 디폴트 메서드가 부담을 줄여준다.

• java.util.function 패키지 하위에 총 43개의 표준 함수형 인터페이스가 존재한다.

• 아래는 6개의 기본적인 인터페이스이다. 이 6개로부터 나머지 57개를 유추해 낼 수 있음

인터페이스 함수 시그니처 의미 예시
UnaryOperator<T> T apply(T t) 반환값과 인수의 타입이 같은 함수, 인수는 1개 String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) 반환값과 인수의 타입이 같은 함수, 인수는 2개 BigInteger::add
Predicate<T> boolean test(T t) 한 개의 인수를 받아서 boolean을 반환하는 함수 Collection::isEmpty
Function<T,R> R apply(T t) 인수와 반환 타입이 다른 함수 Arrays::asList
Supplier<T> T get() 인수를 받지 않고 값을 반환, 제공하는 함수 Instant::now
Consumer<T> void accept(T t) 한 개의 인수를 받고 반환값이 없는 함수 System.out::println

• 표준 함수형 인터페이스 사용할 때 주의할 점

    ◦  대부분 표준 함수형 인터페이스는 기본 타입만 지원한다. (int 등)

    ◦  그렇다고 박싱된 기본 타입을 넣어 사용하면 안 된다.

    ◦  동작은 하지만 계산이 많아지는 경우 성능이 매우 느려질 수 있다.


표준 함수형 인터페이스 대신 직접 구현한 것을 사용해야 하는 경우

• 필요한 용도에 맞는 표준 함수형 인터페이스가 없는 경우

• 구조적으로 동일한 함수형 인터페이스가 있더라도 직접 작성해야 하는 경우

    ◦  Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T,U> 와 동일하다.

         -  인자 두 개를 받아서(Bi), 정수형을 반환하는(ToInt) 함수 : 인수와 반환 타입이 다른 함수(Function)이다.

// Comparator
@FunctionInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

// ToIntBiFunction
@FunctionalInterface
public interface ToIntBiFunction<T, U> {
    int applyAsInt(T t, U u);
}

    ◦  표준형 함수형 인터페이스 ToIntBiFunctionComparator 이후에 등장했지만,

          그래도 Comparator를 사용해야 하는 경우가 많다.

         -  Comparator 는 네이밍이 훌륭하다. 지금의 이름이 API에서 자주 사용되는 그 용도를 잘 설명해주고 있다.

         -  구현하는 쪽에서 반드시 지켜야 할 규약을 담고 있다.

         -  비교자들을 변환하고 조합해주는 유용한 디폴트 메서드를 많이 가지고 있다.

 

• 이처럼 아래 3가지 중 하나 이상에 해당된다면 직접 함수형 인터페이스를 구현할지 고민해도 좋다.

    ◦  자주 쓰이며, 이름 자체가 용도를 명확히 설명해준다.

    ◦  반드시 따라야 하는 규약이 있다.

    ◦  유용한 디폴트 메소드를 제공한다.

 

• 단, 구현하는 것이 인터페이스임을 명심하고 주의하여 구현해야 한다.


@FunctionInterface 어노테이션

• 이 어노테이션이 달린 인터페이스는 해당 인터페이스가 람다용으로 설계된 것임을 의미한다.

• 또한 해당 인터페이스가 오직 하나의 추상 메서드만을 가지고 있어야 한다는 것을 알려준다

    ◦  그렇지 않으면 컴파일되지 않는다 : 누군가 실수로 메서드를 추가하지 못하게 막아준다.

• 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 어노테이션을 붙여주자.


함수형 인터페이스를 사용할 때 주의할 점

• 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안 된다.

• 클라이언트에게 모호함을 주며 헷갈리기 때문에 문제가 발생할 소지가 많다.

public interface ExecutorService extends Executor {
    // Callable<T>와 Runnable을 각각 인수로 하여 다중정의했다.
    // submit 메서드를 사용할 때마다 형변환이 필요해진다.
    <T> Future<T> submit(Callback<T> task);
    Future<?> submit(Runnable task);
}

명명 패턴보다 Annotation을 사용하라

명명 패턴

• 원래는 도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 명명 패턴을 적용해왔다.

    ◦  예) JUnit은 ver.3 까지 테스트 메소드 이름은 test로 시작해야 했다.

 

명명 패턴의 단점

• 오타가 나면 메소드가 무시된다.

    ◦  예) 테스트 메소드 앞에 test 를 붙여야 하는데 tast 를 붙이면 해당 테스트 메소드는 무시된다.

    ◦  개발자는 테스트 메소드가 무시된 것을 테스트 메소드가 통과한 것으로 오해할 수 있다.

• (JUnit의 경우) 개발자가 뭐 앞에 test 만 붙이면 다 되는줄 안다.

    ◦  메소드가 아닌 클래스 앞에 Test 를 붙여놓고, 해당 클래스에 정의된 메소드들이 테스트 메소드로 실행되길 바란다면?

    ◦  JUnit은 클래스 이름에는 관심이 없으며, 경고 메세지조차 출력하지 않는다.

    ◦  개발자는 역시나 메소드들이 모두 무시된 것을, 테스트 메소드가 모두 통과한 것으로 오해할 수 있다.

• 매개변수를 사용할 수 없다.

    ◦  특정 예외를 던져야만 성공하는 테스트가 있는 경우

    ◦  기대하는 예외 타입을 테스트에 알려줘야 하지만 방법이 없다.

 

단점을 어떻게 해결해야 하나?

• Annotation을 사용하면 명명 패턴이 가지는 위의 문제들을 해결할 수 있다.


Annotation

마커 어노테이션 (Marker Annotation)

• 아무 매개변수 없이 단순히 달아주기만 하는 어노테이션

• 어노테이션에 오타를 내거나, 지정된 위치(메소드 위 등) 외 잘못된 위치에 어노테이션을 다는 경우 컴파일 오류가 발생하기 때문에

   기존 명명 패턴에서 발생하던 문제를 해결할 수 있다.

• 테스트 메소드용 어노테이션을 직접 정의해보자.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { ... }

• @Retention(RetentionPolicy.RUNTIME)

    ◦  @Test 가 런타임에도 유지되어야 한다는 표시. 생략하면 테스트 도구는 @Test 를 인식할 수 없다

• @Target(ElementType.METHOD)

    ◦  @Test 가 반드시 메서드 선언에서만 사용되어야 한다는 뜻

 

• 이렇게 어노테이션에 달 수 있는 어노테이션을 메타 어노테이션 이라고 한다.

public class Sample {

    @Test
    public static void m1() { } // 성공
    public static void m2() { } // 무시

    @Test
    public static void m3() { throw new RuntimeException("실패"); } // 실패
    public static void m4() { } // 테스트가 아니다.

    @Test
    public void m5() { } // 잘못된 사용 (정적 메서드가 아님)
    public static void m6() { } // 무시

    @Test
    public static void m7() { throw new RuntimeException("실패"); } // 실패
    public static void m8() { } // 무시

}

• @Test 가 Sample 클래스에 직접적인 영향을 주는 것은 아니다.

• 그저 이 Annotation 에 관심있는 프로그램에게 추가 정보를 제공할 뿐

• 즉, @Test 의 역할 = Sample 클래스의 의미는 그대로 둔 채, 외부에서 이 Annotation 을 이용하여 추가적인 처리를 할 수 있도록 해줌

import java.lang.reflect.*;

public class RunTests {
    public static void main(String[] args) throws Exception {

        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]); // 클래스 이름을 받음

        for (Method m : testClass.getDeclaredMethods()) { // 모든 메소드를 돌면서
            if (m.isAnnotationPresent(Test.class)) { // @Test 가 달려있는지 확인
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " 실패: " + exc);
                } catch (Exception exc) {
                    System.out.println("잘못 사용한 @Test: " + m);
                }
            }
        }

        System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
    }

}

• command line 에서 클래스 이름을 받아 해당 클래스에 존재하는 메소드들을 전부 돌면서 @Test Annotation이 달려있는지를

  확인한다.

• @Test Annotation이 달려있는 메소드인 경우 해당 메소드를 실행시키며, 실행된 메소드에서 예외가 발생되면 해당 예외를

   InvocationTargetException 으로 받아 반환하고, InvocationTargetException 외의 예외가 발생하는 경우 이는 @Test

   잘못 사용한 것이므로 이를 출력하여 알려준다.

 

매개변수를 가지는 어노테이션

• 특정 예외를 던져야만 성공하는 테스트 메소드용 어노테이션을 정의해보자.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

• 위 어노테이션의 매개변수 타입은 Class<? extends Throwable> 이다.

• 와일드카드 → Throwable을 확장한 클래스 객체 의미

    ◦  즉 모든 Exception 을 매개변수로 받을 수 있다.

public class Sample2 {

    @ExceptionTest(ArithmeticException.class) // 분모가 0일때 발생하는 에러
    public static void m1() {  // 성공 (ArithmeticException 예외발생)
        int i = 0;
        i = i / i;
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m2() {  // 실패 (ArithmeticException 말고 다른 예외발생)
        int[] a = new int[0];
        int i = a[1]; // NullPointerException
    }

    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // 실패 (예외가 발생하지 않았음)

}

• 새로 만든 어노테이션을 메소드에 달아주었다.

• 모든 메소드는 ArithmeticException 예외를 반환해야 한다.

if (m.isAnnotationPresent(ExceptionTest.class)) {
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    }
    
    catch (InvocationTargetException wrappedEx) {
        Throwable exc = wrappedEx.getCause(); // exc = 실제 발생한 예외
        Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value(); // excType = 어노테이션에 명시된 예외
        if (excType.isInstance(exc)) passed++; // 어노테이션에 명시한 예외가 발생됨
        else System.out.printf("테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n", m, excType.getName(), exc); // 어노테이션에 명시한 것과 다른 예외가 발생함
    }
    
    catch (Exception exc) {
        System.out.println("잘못 사용한 @ExceptionTest: " + m);
    }
}

• 어노테이션 매개변수 값을 추출하여 테스트 메소드가 반환한 예외와 어노테이션에서 매개변수로 명시된 예외가 동일한지를 비교해준다.

• 명시된 예외가 아닌 다른 예외가 반환되는 경우 : 실패로 처리

• 아무런 예외가 반환되지 않는 경우 : 실패로 처리

• InvocationTargetException 이외의 예외가 발생하는 경우 : 어노테이션을 잘못 사용함

 

배열 매개변수를 가지는 어노테이션

• 예외를 여러개 명시한 다음 그 중 하나만 발생해도 OK 인 로직을 구현하려면 어떻게 해야할까?

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}
@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
public static void doublyBad() { 
    List<String> list = new ArrayList<>();
    list.addAll(5, null); // list의 5번 인덱스부터 null 에 들어있는 값을 add 해라
}

• 매개변수를 여러개 넘길 때 { } 로 감싸주기만 하면 된다.

if (m.isAnnotationPresent(ExceptionTest.class)) {
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    }
    
    catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
        
        // 발생한 예외가 어노테이션 매개변수 배열 안에 들어있는지 for문을 돌면서 검사
        for (Class<? extends Throwable> excType : excTypes) {
            if (excType.isInstance(exc)) { passed++; break; }
        }
        
        if (passed == oldPassed) System.out.printf("테스트 %s 실패: %s %n", m, exc);
    }
}

 

배열 매개변수 대신 @Repeatable 을 사용하는 방법

• 어노테이션 선언시 @Repeatable을 메타 어노테이션으로 달아주면 동일한 어노테이션을 여러번 사용할 수 있다.

• 단, 어노테이션 선언 방법이 조금 달라진다.

    ◦  예를 들어 아래와 같이 어노테이션 A 에 @Repeatable 을 달아서 정의하려 한다고 하자.

@Repeatable
public @interface A { ... }

    ◦  먼저 어노테이션 A 를 반환하는 컨테이너 어노테이션을 정의해야 한다.

    ◦  그 다음 컨테이너 어노테이션 객체를 A 의 매개변수로 넣어줘야 한다.

          -  이 때 컨테이너 어노테이션 은 A 타입 배열을 반환하는 value 메소드 를 가지고 있어야 한다.

          -  또한 컨테이너 어노테이션 에는 적절한 @Retension 과 @Target 이 명시되어야 한다.

          -  이걸 지키지 않으면 컴파일 되지 않는다.

// 1. @Repeatable을 달고 있는 Annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

// 2. 위 어노테이션 객체 배열을 반환하는 '컨테이너 어노테이션'
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}

• 이렇게 되면 어노테이션에 배열을 매개변수로 넣는 방식 대신, 어노테이션을 여러번 달아주는 방식으로 사용할 수 있다.

@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
    List<String> list = new ArrayList<>();
    list.addAll(5, null);
}

• 가독성이 조금 더 좋아진다!

• 어노테이션을 한번만 달면 단일 어노테이션 타입이 적용되지만, 이렇게 여러번 달면 컨테이너 어노테이션 타입이 적용된다.

• 즉 위와 같은 경우 아래와 같은 T/F 값이 반환된다.

doublyBad.isAnnotationPresent(ExceptionTest.class) // false
doublyBad.isAnnotationPresent(ExceptionTestContainer.class) // true

• 즉 어노테이션이 몇 번 달려 있는지 상관없이, @ExceptionTest 를 달고 있는 메소드를 다 골라보려면

    두가지 상황을 모두(|| 사용) 고려해 줘야 한다.

if (m.isAnnotationPresent(ExceptionTest.class) ||
    m.isAnnotationPresent(ExceptionTestContainer.class)) {
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        ExceptionTest[] excTests =
                m.getAnnotationsByType(ExceptionTest.class);
        for (ExceptionTest excTest : excTests) {
            if (excTest.value().isInstance(exc)) {
                passed++;
                break;
            }
        }
        if (passed == oldPassed) System.out.printf("테스트 %s 실패: %s %n", m, exc);
    }
}

• 즉 @Repeatable을 사용하면 가독성은 좋아지지만, 어노테이션을 처리하는 부분에서 코드 처리가 복잡해질 수 있다.


정리

• 어노테이션으로 처리할 수 있는 상황이라면 명명 패턴을 사용하지 말자.

• 사실 도구 제작자를 제외하고는 일반 프로그래머가 어노테이션을 직접 정의할 일은 잘 없다!

• 하지만 어노테이션을 사용하는 사람이라면 그 작동 원리를 알아둘 필요가 있다!