JAVA8에 대해 알아보자(2)

java8

이전에 FunctionalInterface와 람다표현식을 배웠다. 아직 못봤거나 까먹었다면 여기에가서 다시 보자!

Stream

Stream 에는 많은 기능이 있다. 다 설명할 수는 없지만 중요한 몇가지를 공부해보겠다.
자바의 컬렉션들은 Stream을 사용 할 수 있다. 혹은 스트림을 만들 수 있다.
흔히 쓰는 List<T>를 예로 들어 공부해보겠다.
그전에 알아두어야 할 용어가 있다.
Intermediate Operation MethodTerminal Operation Method이다.
Intermediate Operation Method는 중간단계로써 스트림을 리턴하기 때문에 계속 Method Chaining 통해 지시 할 수 있다.
그리고 Terminal Operation Method는 스트림을 끝내는 메소드다. Stream은 Lazy 하기 때문에 이 메소드가 호출되면 그때서야 Intermediate Method를 실행한다.

Optional<T> reduce(BinaryOperator<T> accumulator)
void forEach(Consumer<? super T> action)
Optional<T> max(Comparator<? super T> comparator)
long count()
Optional<T> findFirst()

등이 Terminal Operation Method에 속한다.
한번 코드를 보자!

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
System.out.println(numbers.stream().reduce((i, j) -> i + j).get());
numbers.stream().forEach(x -> System.out.print(x + " "));
System.out.println();
System.out.println(numbers.stream().max((i, j) -> i.compareTo(j)).get());
System.out.println(numbers.stream().min((i, j) -> i.compareTo(j)).get());
System.out.println(numbers.stream().count());
System.out.println(numbers.stream().findFirst().get());

두 번째의 reduce는 합치는 작업을 한다. i + j의 합이 다시 i 가 되고 j 는 다음번숫자로 넘어가는걸 반복한다.
forEach는 우리가 흔히 썼던 for each 문과 같다.
max는 말그대로 최대값을 찾는다. min도 마찬가지다.
count는 스트림안에 개수를 리턴한다.
findFirst는 첫 번째 있는 값을 가져온다.

Stream<T> filter(Predicate<? super T> predicate)
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Stream<T> sorted()
Stream<T> limit(long maxSize)

등이 Intermediate Operation Method에 속한다. 이 외에도 많이 있다.
마찬가지로 코드로 보자!

numbers.stream().filter(i -> i > 5).forEach(x -> System.out.print(x + " "));
System.out.println();
numbers.stream().map(i -> i * 2).forEach(x-> System.out.print(x + " "));
System.out.println();
numbers.stream().sorted((i, j) -> j.compareTo(i)).forEach(x -> System.out.println(x));
numbers.stream().limit(4).forEach(x -> System.out.println(x));

filter는 말그대로 걸러주는 역할을 한다. 파라미터가 Predicate이다 Predicate은 boolean 값을 리턴한다.
map은 어떤타입을 다른타입 혹은 같은타입으로 변환해준다. 위 코드는 2배를 해주는 코드다.
flatMap은 map이랑 같지만 Stream타입을 리턴한다.
sorted는 정렬을 해주는 함수다. 위의 코드는 내림차순으로 정렬한 코드다.
limit는 개수는 제한하는 코드다 mysql의 limit와 같다.
그럼 한번 응용을 해보자.
1~9까지중 3보다 큰값을 골라 2배를 해주고 내림차순으로 정렬뒤 3개만출력하는 코드를 구현해보자. 간단하게 구현 할 수 있다.

numbers.stream()
        .filter(i -> i > 3)
        .map(i -> i * 2)
        .sorted((i,j) -> j.compareTo(i))
        .limit(3)
        .forEach(x -> System.out.println(x));

간단하게 Stream에 대해서 알아봤다.
어느정도 감만 잡았다면 성공한거다.
다음 시간엔 method reference 대해서도 알아보겠다.

JAVA8에 대해 알아보자(1)

java8

자바8의 새롭게 바뀐 부분이 많지만 그 중 내가 자주 쓰는걸 정리 해보겠다.

@FunctionalInterface

첫 번째로 @FunctionalInterface 인터페이스다.
람다를 쓰긴 위한 애노테이션이다. 만약 저 애노테이션이 붙은거라면 람다 표현식을 사용 할 수 있다.
하지만 명시적으로 지정 하지 않더라도 abstract 메소드가 한개라면 람바 표현식을 사용 할 수 있다.
만약 저 애노테이션을 사용한다면 abstract 메소드가 2개 이상 있을경우 컴파일 타임에 에러가 난다.
자바 기본 패키지에 있는 Function이라는 인터페이스다.

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    //...
}

한번 커스텀한 interface를 만들어보자.
두개의 같은 타입 파라미터를 넘겨서 두개를 더하는 인터페이스다.

@FunctionalInterface
interface AddFunction<T> {

    T apply(T t, T t1);
}

사용해 보자

AddFunction<Integer> add = (i, j) -> i + j;
System.out.println(add.apply(10, 20));

AddFunction<String> add1 = (i, j) -> i + j;
System.out.println(add1.apply("hello", " world"));

결과는 다음과 같을 것이다.

30
hello world

lambda

두 번째론 람다표현식이다. 자바 개발자 한테는 조금 생소한 표현식 이다. 하지만 다른 언어들은 일찌감치 람다 표현식을 사용하고 있다.
스칼라, 파이썬 하스켈, c#등 여러가지 언어에서 사용 하고 있다.
한번 살펴보자.
우리가 쓰레드를 사용 할때 이용하는 인터페이스중 Runnable가 있다. 이 것도 java8에선 FunctionalInterface로 구현되었다.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

우리가 java8 이전에는 이렇게 사용하곤 했다. 우리가 흔히 말하는 익명 클래스다. 예를 든 코드다 실질적으로 이렇게 사용하진 않는다.

Runnable runnable = new Runnable() {
     @Override
     public void run() {
         System.out.println("run 호출");
     }
};
runnable.run();

그런데 람바를 쓰면 아주 간단하게 바뀐다. 한번보자.

Runnable runnable = () -> System.out.println("run 호출");
runnable.run();

6줄을 단 한줄로 바꿔버렸다. 대단하다.
그럼 어떻게 바뀌는지 한번 보자.
() 이거는 파라미터다 Runnableabstract 메소드 run은 받는 파라미터가 한개도 없다. 그래서 괄호만 해주는거다.
-> 애로우?라 부른다? 람다? 인가? 흠 그리고 다음 나오는게 메소드 바디이다. 간단하다.
그럼 다음 예제도 살펴보자
이번엔 아까 위에서 봤던 Function 인터페이스다. 이 인터페이스를 아마 자주 사용할 듯 하다.
일단 람다를 쓰지말고 한번 해보자.

Function<Integer,String> function = new Function<Integer, String>() {
    @Override
    public String apply(Integer integer) {
        return String.valueOf(integer);
    }
};
String str = function.apply(1010);
System.out.println(str);

일단 Function 인터페이스는 제네릭타입이다. 첫 번째는 파라미터타입 두 번째는 리턴 타입이다.
한마디로 어떤 타입의 파라미터로 넘기면 다른 타입을 반환하는걸 의미한다. 같은 타입을 지정해도 상관없다. 이런걸 identity라 부른다.
위의 코드는 int를 String으로 변환하는 코드다.
이제 람다로 바꿔보자!

Function<Integer,String> function = x -> String.valueOf(x);
String str = function.apply(1010);
System.out.println(str);

이것도 아주 깔끔하고 짧게 바뀌었다.
다시 한번 살펴보자. apply 메소드는 파라미터가 한개다.그래서 x라는 변수 한개를 선언했다.(파라미터가 한개일땐 ()로 감싸주지 않아도 된다.)
그리고 그 x로 String.valuOf를 이용하여 변환 하였다.
그런데 String.valueOf를 호출하기전에 그 파라미터가 어떤건지 한번 Print로 출력하고 싶다.
그럼 메소드 바디를 감싸주면 된다. 컬리브레이스로 감싸자 해보자.

Function<Integer,String> function = x -> {
    System.out.println(x);
    return String.valueOf(x);
};

이외에도 자주 쓰는 인터페이스는
Consumer<T> 어떤 타입의 파라미터로 받는다 리턴은 없다.
Predicate<T> 어떤 타입의 파라미터로 받는다 리턴은 Boolean 이다.
Supplier<T> 어떤 타입으로 리턴한다. 파라미터는 없다.

이 외에도 java.util.function 패키지에 가보면 function 인터페이스가 있다. 한번씩 보자!
다음 시간에 Stream을 공부해보겠다.

spring-boot-rest를 해보자!(2)

Spring boot rest 를 이용하여 API 서버를 개발해보자! (2)

1편은 여기

검색을 할때 url에 메소드명이 마음에 들지 않는다. 또한 json 키도 마음에 들지 않는다. 그래서 바꾸고싶다.
그러기 위해선 아래와같이 추가해보자

    @RestResource(path = "nameStartsWith", rel = "name")
    Page<Account> findByNameStartsWith(@Param("name") String name, Pageable pageable);

브라우저로 열어보자
http://localhost:8080/account/search

{
  "_links": {
    "name": { //name으로 변경
      "href": "http://localhost:8080/account/search/nameStartsWith{?name,page,size,sort}", //nameStartsWith 으로 변경
      "templated": true
    },
    "findByname": {
      "href": "http://localhost:8080/account/search/findByname{?first_name,page,size,sort}",
      "templated": true
    },
    "self": {
      "href": "http://localhost:8080/account/search"
    }
  }
}

이렇게 나온걸 확인할 수 있다. 그리고 http://localhost:8080/account/search/nameStartsWith?name=wonwoo 로 들어가보면 wonwoo로 시작하는 데이터가 두개 나올 것이다.
아니 근데 나는 이메일을 안보여주고 싶은 경우도 있다. 흠!
그렇담 아래와 같이 인터페이스 추가하자

@Projection(name = "noEmail", types = { Account.class })
public interface NoEmailAccount {

    String getId();

    String getName();

    String getPassword();
}

그런다음 http://localhost:8080/account/1?projection=noEamil url로 브라우저를 열어보자
그러면 아래와 같이 email이 빠진걸 확인 할 수 있다.

{
  "name": "wonwoo",
  "id": "1",
  "password": "qwer",
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

다음은 Address라는 엔티티를 추가해서 Account 에 주소로 사용할 예정이다.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    @Id
    @GeneratedValue
    @Column(name = "address_id")
    private Long id;

    private String street;

    private String state;

    private String country;
}

그리고 Account 엔티티에 다음과 같이 추가 하자

    @OneToOne
    @JoinColumn(name="address_id")
    private Address address;

그리고 초기화 데이터를 넣어주자!

insert into address(address_id, street, state, country) values(1L, '분당구', '경기도', '대한민국');
insert into address(address_id, street, state, country) values(2L, '강남구', '서울특별시', '대한민국');
insert into account(id, name, email, password, address_id) values(1,'wonwoo','wonwoo@test.com','qwer', 1L);
insert into account(id, name, email, password, address_id) values(2,'kevin','kevin@test.com','asdf', 2L);
insert into account(id, name, email, password, address_id) values(3,'wonwoo1','kevin@test.com','qwqw',1L);

그리고 나서 http://localhost:8080/account/1 확인해보자

{
  "name": "wonwoo",
  "email": "wonwoo@test.com",
  "password": "qwer",
  "address": {
    "street": "분당구",
    "state": "경기도",
    "country": "대한민국"
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

이렇게 제대로 나올 것이다. 흠. 근데 address가 마음에 들지 않는다. 한줄로도 나왔으면 좋겠다.
그럼 아까 했던 @Projection을 응용해서 보자
아래와 같이 FullAddress라는 인터페이스 생성한다.

@Projection(name = "fullAddress", types = { Account.class })
public interface FullAddress {

    @Value("#{target.address.country} #{target.address.state} #{target.address.street}")
    String getFullAddress();

    String getName();

    String getEmail();

    String getPassword();
}

그리고 나서 http://localhost:8080/account/1?projection=fullAddress 확인해보면 아래와 같이 한줄로 나왔다.

{
  "name": "wonwoo",
  "password": "qwer",
  "email": "wonwoo@test.com",
  "fullAddress": "대한민국 경기도 분당구",
  "_links": {
    "self": {
      "href": "http://localhost:8080/account/1"
    },
    "account": {
      "href": "http://localhost:8080/account/1{?projection}",
      "templated": true
    }
  }
}

그럼 spring-data-rest-jpa 여기서 마치겠다.
소스는 https://github.com/wonwoo/spring-data-rest-jpa 다운 받을 수 있다
다음엔 몽고도 알아보겠다.