프로그래밍언어 자바 Part-1

5. 자바 중급 활용-2

이번 강좌에서는 자바를 좀 더 제대로 사용하기위해 java.time 패키지의 LocalDate, LocalDateTime 클래스등을 사용해 날짜와 시간등 을 다루는 방법을 알아 봅니다. 또한 제네릭을 통해 타입을 일반화 하는 방법을 배우고 자료구조 프로그래밍에 활용할 수 있습니다.

이 강의를 통해 자바언어의 중급활용을 통해 기초를 다지고 보다 복잡한 자바프로그램 구조를 이해하고 개발 할 수 있습니다.



01: 날짜와 시간 다루기

프로그램에서 날짜와 시간을 다루는 부분은 간단해 보이지만 사실 꽤 번거로운 작업입니다. 문자열과 유사하게 날짜와 시간역시 기본적인 구조와 동작원리를 이해하고 주요 API의 사용법을 익혀서 제대로 사용해야 합니다.

날짜와 시간은 현재 뿐만 아니라 과거, 미래의 시간등이 모두 다뤄질 수 있는데 윤년 계산을 비롯해 국가별로 일광절약시간제 즉 써머타임 등의 적용 유무가 다르며 특히 과거의 경우 역사적으로 시간과 관련된 변경 사항들이 많아 단순하게 생각하기 어려운 부분이 있습니다.(예를들어 대한민국의 시간대는 1961년 8월10일 UTC+8:30 에서 UTC+9 로 변경됨)

기본적으로 자바에서는 java.util.Datejava.util.Calendar 클래스를 사용했지만 사용에 불편함이 많아 JDK8 이후부터는 java.time 패키지의 클래스로 많은 부분이 대체 되었습니다.

현재는 대부분의 날짜와 시간관련 처리는 java.time 패키지를 통해 처리할 수 있으므로 이를 중심으로 학습하면 됩니다. 또한 날짜를 화면에 표현하기 위해서는 적절하게 형식을 지정해 주어야 하므로 형식 지정 클래스들도 잘알고 있어야 합니다.

날짜, 시간 구하기

LocalDate, LocalTime, LocalDateTime

가장 기본이 되는 클래스들로 각각 날짜, 시간, 날짜와시간 을 다루는 클래스 입니다. new()를 통해 직접적인 인스턴스 생성이 불가능 하고 static 메서드를 이용하는 방식을 사용 합니다.

LocalDate d1 = LocalDate.now();
LocalDate d2 = LocalDate.of(2019,10,10);
LocalTime t1 = LocalTime.now();
LocalTime t2 = LocalTime.of(7, 20,20);

» 실습: 기본적인 날짜 시간 구하기

실습개요

LocalDate, LocalTime, LocalDateTime 을 이용한 기본적인 날짜, 시간등을 구하는 방법을 실습합니다.

소스코드

public class DateTimeTest1 {
    public static void main(String[] args) {
        LocalDate d1 = LocalDate.now();
        LocalDate d2 = LocalDate.of(2019, 10, 10);

        LocalTime t1 = LocalTime.now();
        LocalTime t2 = LocalTime.of(7, 20, 20);

        System.out.printf("LocalDate.now() : %s\n", d1);
        System.out.printf("LocalDate.of(2019,10,10) : %s\n", d2);
        System.out.printf("LocalTime.now() : %s\n", t1);
        System.out.printf("LocalTime.of(7,20,20) : %s\n", t2);

        LocalDateTime dt1 = LocalDate.now().atTime(LocalTime.MIDNIGHT);
        LocalDateTime dt2 = LocalDate.now().atTime(LocalTime.MAX);

        System.out.printf("LocalDate.now().atTime(LocalTime.MIDNIGHT) : %s\n", dt1);
        System.out.printf("LocalDate.now().atTime(LocalTime.MAX) : %s\n", dt2);
    }
}

실행결과

별도의 형식을 지정하지 않았기 때문에 해당 객체에서 제공하는 기본 형식으로 출력됩니다. 2019-07-28T00:00 과 같은 형식은 UTC 시간이라고 하는 협정세계시간의 표기법 입니다.

LocalDate.now() : 2019-07-28
LocalDate.of(2019,10,10) : 2019-10-10
LocalTime.now() : 18:09:55.505280
LocalTime.of(7,20,20) : 07:20:20
LocalDate.now().atTime(LocalTime.MIDNIGHT) : 2019-07-28T00:00
LocalDate.now().atTime(LocalTime.MAX) : 2019-07-28T23:59:59.999999999

형식 지정하기

java.time.format 패키지의 클래스들이 사용되며 대표적인 것은 DateTimeFormatter 클래스 입니다.

LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); // 2015-04-18 00:42:24
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); // 

날짜와 시간차이 계산하기

보통 날짜와 시간이 많이 사용되는 부분은 월별로 데이터를 관리하거나 특정 기간의 데이터를 조회하거나 하는등의 작업 입니다.

LocalDate.of(2019, 5, 15).plus(Period.ofDays(1)); // (2019년5월15일 + 1일간) = 2019년5월16일
LocalTime.of(9, 0, 0).plus(Duration.ofMinutes(10)); // (9시 + 10분간) = 9시10분
LocalDate.now().plusDays(1); // (오늘 + 1일) = 내일 
LocalTime.now().minusHours(3); // (지금 - 3시간) = 3시간 전
Period period = Period.between(startDate, endDate); // 두 날짜 사이의 연/원/일 계산

» 실습: 날짜 형식 지정 및 시간차이 계산

실습개요

DateTimeFormatter 를 이용한 형식 지정 및 Period 와 Duration 클래스를 사용한 날짜 , 시간 차이 계산 및 조정 예제 입니다.

소스코드

public class DateTimeTest2 {
    public static void main(String[] args) {
        LocalDateTime dt1 = LocalDateTime.now();
        DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);

        System.out.println(dtf.format(dt1));
        System.out.println(dt1.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        System.out.println(dt1.plusDays(2));
        System.out.println(LocalTime.now().minusHours(3));
        System.out.println(Duration.ofMinutes(10).getSeconds());

        LocalTime start = LocalTime.of(11, 40, 50);
        LocalTime end = LocalTime.of(11, 42, 50);

        Duration duration = Duration.between(start, end);

        System.out.println("Seconds: " + duration.getSeconds());

        LocalDate startDate = LocalDate.of(1950, 9, 1);
        LocalDate endDate = LocalDate.of(2010, 9, 2);
        Period period = Period.between(startDate, endDate);

        System.out.println("Years: " + period.getYears());
        System.out.println("Months: " + period.getMonths());
        System.out.println("Days: " + period.getDays());
    }
}

실행결과

19. 7. 28. 오후 6:18
2019-07-28 18:18:33
2019-07-30T18:18:33.905151
15:18:33.951556
600
Seconds: 70
Nano Seconds: 800
Years: 60
Months: 0
Days: 1


02: 제네릭(Generics)

타입 안전성(Type Safe)

객체지향 프로그램 개발에 있어 타입의 중요성은 말할 필요도 없다. 고정된 코드에서는 크게 문제가 되지 않지만 실행과정에서 동적으로 전달되는 객체를 참조해야 하는 경우 잘못된 타입이 전달되면 문제가 됩니다.

조금 다른 경우이기는 하지만 예를 들어 다음코드는 문제가 있습니다.

Color color = new Color("red");

이 경우에는 6장에서 배우게될 enum 을 사용하는 것이 바람직 합니다.

Color color = new Color(Color.RED);

이과 같이 객체 타입으로 지정을 해버리면 컴파일시 잘못된 색상을 사용할 가능성이 원천 차단 됩니다. 마찬가지로 다양한 타입으로 데이터가 구성될 수 있는 클래스를 설계할때 제네릭을 사용하면 이와 같은 타입 문제를 해결할 수 있습니다.

제네릭

예를들어 ArrayList 는 배열과 유사한 자료구조를 제공하는 클래스로 Object 타입의 데이터를 저장할 수 있습니다.

그런데 Object 클래스는 모든 자바 클래스의 슈퍼클래스 이므로 실제로는 모든 자바 클래스가 원소로 들어갈 수 있다는 의미가 됩니다. 매우 편한 구조이기는 하지만 ArrayList 로 부터 참조 원소들을 꺼내 사용할때 타입들이 서로 다를 수 있기 때문에 메서드의 사용등이 차이가 있어 타입 비교를 해야하는 문제가 발생 합니다.

이와 같이 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 과정에 타입체크(compile-time type check)를 해주는 기능을 제네릭 이라고 합니다.

제네릭 사용의 장점은 다음과 같습니다.

class Storage<T> {
    T item;
    // getter, setter 생략
}

class App {
    public static void main(String[] args) {
        Storage<String> storage = new Storage<>();
    }
}

제네릭을 사용할때 주의 할 점은 다음과 같습니다.

» 실습: 기본적인 제네릭 클래스 생성과 사용

실습개요

타입 파라미터를 이용해 간단한 제네릭 클래스를 생성하고 사용하는 실습 예제 입니다.

소스코드

public class Storage<T> {
    T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

소스코드

public class GenericsTest1 {
	public static void main(String[] args) {
		Storage<String> storage1 = new Storage<>();
		storage1.setItem("MyItem");
		System.out.println(storage1.getItem());

		Storage<Integer> storage2 = new Storage<>();
		storage2.setItem(20201121);
		System.out.println(storage2.getItem());
	}
}

실행결과

MyItem
20201121

제네릭 고급 사용

제네릭 메서드

이번에는 메서드의 선언에 제네릭이 사용되는 형태를 살펴봅니다. 메서드의 인자 혹은 리턴에 제네릭이 사용될 수 있으며 다양한 타입을 처리해야 하는 경우 유용하게 활용할 수 있습니다.

앞에서 만든 Storage 를 인자로 하는 메서드는 다음과 같이 선언될 수 있습니다.

public <T> void print(Storage<T> storage) {
}

리턴타입 역시 제네릭 클래스를 사용할 수 있습니다.

public List<String> getList() {
}

인자와 리턴이 모두 제네릭을 가지는 경우에는 다소 복잡할 수 있습니다.

public <T> List<Character> convert(Storage<T> storage) {
}

와일드 카드

제네릭 타입을 사용할때 발생할 수 있는 문제점으로 특정 제네릭 타입을 인자로 받는 메서드를 구현하는 상황을 예로 들 수 있습니다. 앞에서 만든 Storage 를 인자로 하는 메서드를 살펴보도록 합니다.

public void print(Storage<String>) {
    ..
}

인자로 String 타입 파라미터를 가지는 Storage 클래스가 지정되어 있기 때문에 Storage 타입은 해당 메서드를 이용할 수 없습니다. 인자가 다르니 메서드 오버로딩을 사용하면 어떨까요 ? 아쉽게도 제네릭의 경우 클래스 타입 자체는 동일하므로 오버로딩이 적용되지 않습니다. 만약 허용된다고 해도 필요한 타입마다 메서드를 오버로딩하는 것도 바람직한 구조는 아닙니다.

이 경우 와일드 카드를 사용해 사용할 수 있는 타입에 유연성을 부여하는 방법이 있습니다.

public void print(Storage<? extends Storage) {

}

» 실습: 제네릭 메서드 및 와일드 카드 사용 예제

실습개요

인자 및 리턴타입에 제네릭 클래스를 사용하는 구조를 익히고 와일드 카드를 이용해 제네릭 타입을 제한하는 방법을 배우기 위한 실습 예제 입니다.

소스코드

import java.util.ArrayList;
import java.util.List;

public class GenericsTest2 {
    public <T> List<Character> convert(Storage<T> storage) {
        ArrayList<Character> list = new ArrayList<>();

        String s = String.valueOf(storage.getItem());
        int size = s.length();
        for (int i = 0; i < size; i++) {
            list.add(s.charAt(i));
        }
        return list;
    }

    public static void main(String[] args) {
        Storage<String> s1 = new Storage<>();
        s1.setItem("MyItem");

        Storage<Integer> s2 = new Storage<>();
        s2.setItem(20201121);

        GenericsTest2 gt2 = new GenericsTest2();

        System.out.println(gt2.convert(s1));
        System.out.println(gt2.convert(s2));
    }
}

실행결과

[M, y, I, t, e, m]
[2, 0, 2, 0, 1, 1, 2, 1]

참고 자료