[Java 1.8] 날짜 정리

Java 1.8 날짜 정리

자바 1.8 이전에는 날짜 연산이 쉽지 않았다. Joda 같은 라이브러리들을 쓰면 된다고 하는데, 필자는 Util 클래스에서 내부적으로 Calendar를 사용하여 연산하고 Date/long/String 간의 변환을 통하여 사용해왔다. 그러던 와중에 자바 1.8의 새로운 날짜들을 보니 신세계가 열렸다. 그래서 간단하게 소개하고 자주 사용될 만한 예제들도 나열하려 한다.

먼저 중요하다고 생각되는 클래스들을 소개하겠다.

클래스

날짜 (Temporal)

Instant : machine time에 유용한 1970년 1월 1일부터 시간을 세는 클래스 (millisecond 뿐만 아니라 nanosecond까지 센다)

LocalDate : [년,월,일]과 같은 날짜만 표현하는 클래스 (시간은 포함하지 않는다)

LocalDateTime : [년,월,일,시,분,초]를 표현하는 클래스 (LocalDate와 함께 가장 많이 쓰이는 클래스가 될 것 같다)

LocalTime : [시,분,초]와 같이 시간만 표현하는 클래스

기간 (TemporalAmount)

Period : 두 날짜 사이의 [년,월,일]로 표현되는 기간 (시간을 다루지 않다 보니 LocalDate를 사용한다)

Duration : 두 시간 사이의 [일,시,분,초]로 표현되는 기간 (Instant 클래스를 사용하고, seconds와 nanoseconds로 측정 되지만 [일,시,분,초]로 변환해 주는 메쏘드를 제공)

기타

ChronoUnit : 한가지의 단위를 표현하기 위한 클래스 (년,월,일,시,분,초 등)

DayOfWeek : 요일

자주 쓰는 메쏘드들

  • 날짜 가져오기

      LocalDate.now(); // 오늘
      LocalDateTime.now(); // 지금
      LocalDate.of(2015, 4, 17); // 2015년4월17일
      LocalDateTime.of(2015, 4, 17, 23, 23, 50); // 2015년4월17일23시23분50초
      Year.of(2015).atMonth(3).atDay(4).atTime(10, 30); // 2015년3월4일 10시30분00초
    
  • 기간 가져오기

      Period.ofYears(2); // 2년간(P2Y)
      Period.ofMonths(5); // 5개월간(P5M)
      Period.ofWeeks(3); // 3주간(P21D)
      Period.ofDays(20); // 20일간(P20D)
    
      Duration.ofDays(2); // 48시간(PT48H)
      Duration.ofHours(8); // 8시간(PT8H)
      Duration.ofMinutes(10); // 10분간(PT10M)
      Duration.ofSeconds(30); // 30초간(PT30S)
    
  • 날짜 + 기간 = 날짜

      LocalTime.of(9, 0, 0).plus(Duration.ofMinutes(10)); // (9시 + 10분간) = 9시10분
      LocalDate.of(2015, 5, 15).plus(Period.ofDays(1)); // (2015년5월15일 + 1일간) = 2015년5월16일
      LocalDateTime.of(2015, 4, 17, 23, 47, 5).minus(Period.ofWeeks(3)); // (2015년4월17일 23시47분05초 - 3주간) = 2015년3월27일 23시47분05초
      LocalDate.now().plusDays(1); // (오늘 + 1일) = 내일
      LocalTime.now().minusHours(3); // (지금 - 3시간) = 3시간 전
    
  • 날짜 - 날짜 = 기간

      Period.between(LocalDate.of(1950, 6, 25), LocalDate.of(1953, 7, 27)); // (1953년7월27일 - 1950년6월25일) = 3년1개월2일간(P3Y1M2D)
      Period.between(LocalDate.of(1950, 6, 25), LocalDate.of(1953, 7, 27)).getDays(); // 3년1개월2일간 => 2일간
      LocalDate.of(1950, 6, 25).until(LocalDate.of(1953, 7, 27), ChronoUnit.DAYS); // 3년1개월2일간 => 1128일간
      ChronoUnit.DAYS.between(LocalDate.of(1950, 6, 25), LocalDate.of(1953, 7, 27)); // 3년1개월2일간 => 1128일간
    
      Duration.between(LocalTime.of(10, 50), LocalTime.of(19, 0)); // (19시00분00초 - 10시50분00초) = 8시간10분간(PT8H10M)
      Duration.between(LocalDateTime.of(2015, 1, 1, 0, 0), LocalDateTime.of(2016, 1, 1, 0, 0)).toDays(); // 365일간
      ChronoUnit.YEARS.between(LocalDate.of(2015, 5, 5), LocalDate.of(2017, 2, 1)); // 1년간
    
  • 날짜 변환하기

    • LocalDate -> String

      LocalDate.of(2020, 12, 12).format(DateTimeFormatter.BASIC_ISO_DATE); // 20201212
      
    • LocalDateTime -> String

      LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); // 2015-04-18 00:42:24
      
    • LocalDateTime -> java.util.Date

      Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()); // Sat Apr 18 01:00:30 KST 2015
      
    • LocalDate -> java.sql.Date

      Date.valueOf(LocalDate.of(2015, 5, 5)); // 2015-05-05
      
    • LocalDateTime -> java.sql.Timestamp

      Timestamp.valueOf(LocalDateTime.now()); // 2015-04-18 01:06:55.323
      
    • String -> LocalDate

      LocalDate.parse("2002-05-09"); // 2002-05-09
      LocalDate.parse("20081004", DateTimeFormatter.BASIC_ISO_DATE); // 2008-10-04
      
    • String -> LocalDateTime

      LocalDateTime.parse("2007-12-03T10:15:30"); // 2007-12-03T10:15:30
      LocalDateTime.parse("2010-11-25 12:30:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); // 2010-11-25T12:30
      
    • java.util.Date -> LocalDateTime

      LocalDateTime.ofInstant(new Date().toInstant(), ZoneId.systemDefault()); // 2015-04-18T01:16:46.755
      
    • java.sql.Date -> LocalDate

      new Date(System.currentTimeMillis()).toLocalDate(); // 2015-04-18
      
    • java.sql.Timestamp -> LocalDateTime

      new Timestamp(System.currentTimeMillis()).toLocalDateTime(); // 2015-04-18T01:20:07.364
      
    • LocalDateTime -> LocalDate

      LocalDate.from(LocalDateTime.now()); // 2015-04-18
      
    • LocalDate -> LocalDateTime

      LocalDate.now().atTime(2, 30); // 2015-04-18T02:30
      
  • 요일로 날짜 가져오기

      LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.SATURDAY)); // 다음 토요일
      LocalDate.of(2016, 5, 1).with(TemporalAdjusters.dayOfWeekInMonth(3, DayOfWeek.SUNDAY)); // 2016년5월 세번째 일요일
      LocalDate.of(2015, 7, 1).with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)); // 2015년7월 첫번째 월요일
    
  • 언어별 출력

      DayOfWeek.MONDAY.getDisplayName(TextStyle.FULL, Locale.ENGLISH); // Monday
      DayOfWeek.MONDAY.getDisplayName(TextStyle.NARROW, Locale.ENGLISH); // M
      DayOfWeek.MONDAY.getDisplayName(TextStyle.SHORT, Locale.ENGLISH); // Mon
    
      DayOfWeek.MONDAY.getDisplayName(TextStyle.FULL, Locale.KOREAN); // 월요일
      DayOfWeek.MONDAY.getDisplayName(TextStyle.NARROW, Locale.KOREAN); // 월
      DayOfWeek.MONDAY.getDisplayName(TextStyle.SHORT, Locale.KOREAN); // 월
    
      Month.FEBRUARY.getDisplayName(TextStyle.FULL, Locale.US); // February
      Month.FEBRUARY.getDisplayName(TextStyle.FULL, Locale.KOREA); // 2월
    

참고

[Java 1.8] 두개의 리스트 조인(병합)하기

자바에서 타입이 다른 두개의 리스트를 조인하고 싶다. 데이터베이스에서 조인을 해서 가져오는 것이 최선이지만 어플에서 조인을 해야하는 경우도 종종 생기는 것 같다. 마침 자바 1.8 에서 제공하는 람다 표현식을 써보고 싶어서 테스트 겸해서 하나 만들어 보았다.

예제

게시판 리스트 화면을 예로 들겠다. 게시글 리스트에서 회원이름을 출력하고 싶다. 게시판인 Article 클래스에는 memberNo를 가지고 있고, Member클래스에는 memberNo와 memberName이 있다. List<Article> 와 List<Member>를 조인해보자.




List<Article>안에 memberName이 전부 null이 였는데, Member 클래스에서 회원번호를 찾아서 이름을 넣어주는 코드다.


List<Article> articles;
List<Member> member;

for (Article article : articles) {
    for (Member member : members) {
        if (article.getMemberNo() == member.getMemberNo()) {
            article.setMemberName(member.getMemberName());
        }
    }
}

이중 루프를 썼더니 잘 된다. 이제 같은 동작을 1.8 의 람다 표현식을 사용해서 구현해보자. CollectionJoiner 라는 클래스를 만들어서 안에서 루프를 돌리고, 외부에서는 조건문과 할당해주는 부분만 넘기면 되게 해보자.

먼저 테스트할 코드부터 작성해보자.


List<Article> newList = CollectionJoiner.collections(articles, members)
    .when((article, member) -> article.getMemberNo() == member.getMemberNo())
    .then((article, member) -> {
        article.setMemberName(member.getMemberName());
        return article;
    });

2중루프와 같은 동작을 하는 CollectionJoiner 클래스를 사용할 때의 코드이다. 위의 코드가 작동하게 하기 위해서는 두가지의@FunctionalInterface를 사용해야 한다.

  • BiPredicate<T, U> 클래스의 boolean test(T, U);
  • BiFunction<T, U, R> 클래스의 R apply(T, U);

이제 아래 클래스를 생성해보자.

CollectionJoiner.java

package com.test;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;

public class CollectionJoiner{

    private CollectionJoiner() {
    }

    public static <T, U> CollectionJoinerHelper<T, U> collections(Collection<T> t, Collection<U> u) {
        return new CollectionJoinerHelper<T, U>(t, u);
    }

    public static class CollectionJoinerHelper<T, U> {

        private Collection<T> t;
        private Collection<U> u;
        private BiPredicate<T, U> condition;

        private CollectionJoinerHelper(Collection<T> t, Collection<U> u) {
            this.t = t;
            this.u = u;
        }

        public CollectionJoinerHelper<T, U> when(BiPredicate<T, U> condition) {
            this.condition = condition;
            return this;
        }

        public <R> List<R> then(BiFunction<T, U, R> function) {

            List<R> list = new ArrayList<>();

            for (T tOne : t) {
                for (U uOne : u) {
                    if (condition.test(tOne, uOne)) {
                        list.add(function.apply(tOne, uOne));
                    }
                }
            }

            return list;
        }

    }

}

클래스를 추가하고 실행해보면, 결과는 이중루프를 돌린 것과 같다.

처음에는 CollectionJoiner 같은 클래스로 만들면 사용할 때, 코드 양이 줄어들 줄 알았는데, 비슷한 것 같다. 그래도 자바 1.8의 람다를 연습하는 데에는 도움이 되었다.
지금은 then을 호출하자마자 List<R>를 리턴하는데, 나중에는 리턴타입도 Collectors 클래스를 통해서 정할 수 있도록 만들어 봐야겠다.

[Java] Http 요청 보내기 (HttpClient)

개요

자바스크립트에서는 태그에 넣으면 host, pathname, protocol, port, search 등을 지원해서 url 관리하기가 편리하다.

하지만 자바에서는 Url을 핸들링하기가 쉽지않다. 그래서 아파치에서 제공하는 유틸을 소개하려 한다.

예제

  • Uri 생성하기

    먼저 http 요청을 하게 해주는 라이브러리를 추가해야 한다.

      <dependency>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpclient</artifactId>
          <version>4.3.5</version>
      </dependency>
    

    그리고 아래와 같이 코드를 작성해준다.

    HttpTest.java

      package com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
    
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.utils.URIBuilder;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
              System.out.println(uri);
    
          }
    
      }
    

    URIBuilder 클래스를 사용해서 처음부터 host, protocol, port 까지 정해주며 생성할 수도 있고, 예제와 같이 파라미터만 추가도 가능하다. 결과는 다음과 같다.

      http://jekalmin.tistory.com?aaa=bbb&ccc=ddd
    
  • http 요청하기

    이제 생성한 uri를 가지고 http 요청을 해보자. 아래와 같이 코드를 추가해준다.

    HttpTest.java

      package cpackage com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
    
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.impl.client.HttpClientBuilder;
      import org.apache.http.util.EntityUtils;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
    
              HttpClient httpClient = HttpClientBuilder.create().build();
              HttpResponse response = httpClient.execute(new HttpGet(uri)); // post 요청은 HttpPost()를 사용하면 된다. 
              HttpEntity entity = response.getEntity();
              String content = EntityUtils.toString(entity);
              System.out.println(content);
    
          }
    
      }
    

    4.0부터는 DefaultHttpClient 클래스를 사용했으나 4.3부터는 deprecated 된 상태이다. 대신 HttpClientBuilder를 사용하여 생성하기를 권장하고 있다.
    http 요청후 내용 받아오는 부분은 원래 entity.getContent() 인데 getContent()는 InputStream을 반환한다. 바로 String으로 받기 위해 EntityUtils를 사용했다.

  • Uri에서 파라미터 파싱하기

    host, port, path 등의 정보는 URI 객체에서 getHost() 등과 같은 메소드로 바로 가져올 수 있다. 하지만 파라미터의 경우는 제공하지 않는다.
    그래서 아파치에서 제공하는 유틸을 사용했다. 아래 예제를 보자.

    HttpTest.java

      package com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
      import java.util.List;
    
      import org.apache.http.NameValuePair;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.client.utils.URLEncodedUtils;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
              System.out.println(uri);
    
              List<NameValuePair> paramList = URLEncodedUtils.parse(uri, "utf-8");
              for(NameValuePair param : paramList){
                  System.out.println(param.getName() + " = " + param.getValue());
              }
          }
    
      }
    

    결과는 아래와 같다.

      http://jekalmin.tistory.com?aaa=bbb&ccc=ddd
      aaa = bbb
      ccc = ddd
    

    아파치에서는 URLEncodedUtils 라는 클래스를 제공한다. 위의 parse 외에도 거꾸로 변환해주는 format 기능도 있다.
    두 메소드는 오버로딩을 통해 다양한 파라미터 타입을 제공하기도 한다.

결론

예전에는 자바에서 http 요청을 보낼 때, string을 결합하여 uri를 직접 생성하거나 그 작업을 도와주는 유틸을 만들곤 했었는데, 이미 다 제공되고 있었다. EntityUtil과 URLEncodedUtils는 4.0 버전부터 지원한다.
코딩하다가 불편하다고 느낄때 무작정 유틸을 만드는 것은 많은 테스트를 안하면 안전하지 않을 뿐만 아니라 많은 시간이 소요된다. 내가 불편함을 느낀다면, 다른 사람들도 똑같이 느꼈을 것이다. 직접 만드는 것은 최후의 보루로 남겨두자.

Gson을 이용한 json을 객체에 담기

JsonObject -> 객체 변환

Gson 라이브러리는 json으로 받은 데이터를 내가 만든 객체에 자동으로 set 해주는 기능을 제공한다. 먼저 간단한 JsonObject 형태의 json을 객체로 변환해보자.

Member.java

package com.tistory.jekalmin;
public class Member {

    private String id;
    private String name;
    private int age;
    private String address;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    @Override
    public String toString() {
        return "Member [id=" + id + ", name=" + name + ", age=" + age
                + ", address=" + address + "]";
    }



}

GsonTest.java

package com.tistory.jekalmin;

import com.google.gson.Gson;

public class GsonTest {

    public static void main(String[] args) {

        Gson gson = new Gson();
        String jsonString = "{'id':'jekalmin','name':'Min','age':26,'address':'Seoul'}";
        System.out.println(gson.fromJson(jsonString, Member.class));

    }
}

결과는 다음과 같다.

Member [id=jekalmin, name=Min, age=26, address=Seoul]



JsonArray -> List 변환

이번엔 조금더 까다로운 JsonList를 List로 변환해보자. Member.java 클래스는 동일하다.

GsonTest.java

package com.tistory.jekalmin;

import java.util.Arrays;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class GsonTest {

    public static void main(String[] args) {

        Gson gson = new Gson();
        String jsonString = "[{'id':'jekalmin','name':'Min','age':26,'address':'Seoul'},{'id':'park','name':'park','age':27,'address':'Seoul'},{'id':'kim','name':'kim','age':28,'address':'Incheon'}]";

        // 방법1
        Member[] array = gson.fromJson(jsonString, Member[].class);
        List<Member> list = Arrays.asList(array);

        // 방법2
        List<Member> list2 = gson.fromJson(jsonString, new TypeToken<List<Member>>(){}.getType());

        System.out.println(list);
        System.out.println(list2);

    }
}

두 방법의 결과는 같은 것을 알 수 있다.

[Member [id=jekalmin, name=Min, age=26, address=Seoul], Member [id=park, name=park, age=27, address=Seoul], Member [id=kim, name=kim, age=28, address=Incheon]]
[Member [id=jekalmin, name=Min, age=26, address=Seoul], Member [id=park, name=park, age=27, address=Seoul], Member [id=kim, name=kim, age=28, address=Incheon]]

방법은 두가지 중에 아무거나 사용해도 될 것 같다.

java에서 정규표현식으로 원하는 문자 파싱하기

글 쓰기에 앞서 정규표현식의 기본적인 문법을 알고 있다고 가정하겠다. 로그파일이나 html소스등 파싱해야 할 경우가 종종 생기는데 자바스크립트에서 정규표현식을 사용하여 원하는 문자열을 뽑아오는 방법이다.

"jekalmin 25 male 2014-08-22" 라는 로그가 있다고 가정하고 하나씩 뽑아와보자.

먼저 정규표현식을 작성해야 하는데, 작성할 때 유의할 사항은 뽑아내고자 하는 문자열을 괄호로 감싸야 한다.


위 코드의 실행결과는 다음과 같다.


group안에 0번은 매치되는 문자열 전체,

1번부터는 첫번째 괄호, 2번은 두번째 괄호에 상응 하는 문자열을 뽑아온다.

테스트 코드 주소 :

https://github.com/jekalmin/Jekal/blob/master/java/test/RegexpTest.java