Welcome! Everything is fine.

[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 8회차 본문

Java

[Java/Study] 김영한의 실전 자바 중급 1편 - 스터디 8회차

개발곰발 2024. 12. 9.
728x90

 

인프런 강의 <김영한의 실전 자바 - 중급 1편>을 보고 정리한 내용입니다.

매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.


📘열거형 - ENUM

타입 안전 열거형 패턴

강의 예제를 실습하면서, 다음과 같이 문자열을 그대로 입력하는 방식은 몇 가지 문제점이 있었다.

public class StringGrade {
    public static final String BASIC = "BASIC";
    public static final String GOLD = "GOLD";
    public static final String DIAMOND = "DIAMOND";
}

 

1) 오타가 발생하기 쉽다.

2) 유효하지 않는 값이 입력될 수 있다.

3) 다양한 형식으로 입력될 수 있어 일관성이 떨어진다.

 

이러한 문제점을 해결하기 위해 타입 안전 열거형 패턴을 사용한다. 타입 안전 열거형 패턴의 장점은 다음과 같다.

 

1) 타입 안전성 향상 : 잘못된 값을 입력하는 문제 방지 가능

2) 데이터 일관성 : 정해진 객체만 사용하므로 데이터의 일관성 보장

 

타입 안전 열거형 패턴을 사용하는 예시를 보자. 회원이 3개의 등급으로 나뉘고, 등급별 할인률이 각각 다른 상황이다. 각각 별도의 인스턴스를 생성하고, 외부에서 새로운 인스턴스를 생성할 수 없도록 private 생성자를 추가했다.

public class ClassGrade {
    // 각각 회원 등급별로 상수 선언, 별도의 인스턴스 생성
    public static final ClassGrade BASIC = new ClassGrade(); // x001
    public static final ClassGrade GOLD = new ClassGrade(); // x002
    public static final ClassGrade DIAMOND = new ClassGrade(); // x003

    // private 생성자 추가 - 클래스 외부에서 접근X
    private ClassGrade() {}
}

 

이렇게 하면 Sttring이 아닌 ClassGrade라는 타입을 인수로 받을 수 있고, 앞서 열거한 BASIC , GOLD , DIAMOND 상수 만 사용할 수 있다.

public class DiscountService {
    // String이 아닌 ClassGrade라는 타입을 인수로 받음.
    public int discount(ClassGrade classGrade, int price) {
        int discountPercent = 0;

        if (classGrade == ClassGrade.BASIC) {
            discountPercent = 10;
        } else if (classGrade == ClassGrade.GOLD) {
            discountPercent = 20;
        } else if (classGrade == ClassGrade.DIAMOND) {
            discountPercent = 30;
        } else {
            System.out.println("할인X");
        }
        return price * discountPercent / 100;
    }
}

열거형이란?

✔️ 열거형(Enum Type) : 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 클래스로, 서로 관련된 상수들의 집합을 정의하기 위한 자바의 데이터 타입이다.

 

위에서 회원 등급 별로 상수를 선언한 것을 다음과 같이 enum 타입으로 간단히 정의할 수 있다.

public enum Grade {
    BASIC, GOLD, DIAMOND
}

 

여기에서 더 나아가서, discountPercent 필드와 getDiscountPercent() 메서드를 추가하고, 기존의 DiscountService가 가지고 있던 discount() 메서드를 옮겨 리팩토링 할 수 있다. 객체 지향 관점에서 자신의 데이터를 외부에 노출하는 것보단 Grade 클래스가 자신의 할인율을 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.

public enum Grade {
    BASIC(10), GOLD(20), DIAMOND(30);

    private final int discountPercent;

    Grade(int discountPercent) {
        this.discountPercent = discountPercent;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }

    public int discount(int price) {
        return price * discountPercent / 100;
    }
}

 

그리고 DiscountService와 중복을 제거함으로써 더 효율적인 코드로 만들 수 있다.

public class EnumRefMain3_4 {
    public static void main(String[] args) {
        int price = 10000;

        Grade[] grades = Grade.values();
        for (Grade grade : grades) {
            printDiscount(grade, price);
        }
    }

    public static void printDiscount(Grade grade, int price) {
        System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
    }
}
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000

 

✔️ 열거형의 특징

  • 열거형도 클래스이다.
  • enum 키워드를 사용한다.
  • 원하는 상수의 이름을 나열한다.
  • 자동으로 java.lang.enum을 상속받는다.
  • 외부에서 임의로 생성할 수 없다.

✔️ 열거형의 장점

  • 타입 안정성 향상 : 사전에 정의된 상수들로만 구성되므로 유효한 값만 입력된다. 유효하지 않은 값이 들어오면 컴파일 오류가 발생한다.
  • 간결성 및 일관성 : 코드가 더 간결해지고 명확해지며, 데이터 일관성이 보장된다.
  • 확장성 : ENUM에 새로운 상수를 추가하기만 하면 간편하게 확장이 가능하다.

주요 메서드

  • values(): 모든 ENUM 상수를 포함하는 배열을 반환한다.
  • valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환한다.
  • name(): ENUM 상수의 이름을 문자열로 반환한다.
  • ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환한다. 가급적 사용하지 않는 것이 좋다.
  • toString(): ENUM 상수의 이름을 문자열로 반환한다. name() 메서드와 유사하지만, toString() 은 직접 오버라이드 할 수 있다.

📘날짜와 시간

날짜와 시간 라이브러리

날짜와 시간을 계산하는 것은 생각보다 복잡하고 고려해야할 것이 많다.

 

1) 날짜와 시간 차이 계산

2) 윤년 계산

3) 일광 절약 시간(Daylight Saving Time, DST) 변환

4) 타임존 계산

 

따라서 자바에서는 날짜와 시간 라이브러리를 지속적으로 업데이트했다. 다음은 해당 라이브러리 목록이다.

LocalDateTime

국내 서비스만 고려한다면 가장 기본적인 날짜와 시간 클래스는 LocalDate , LocalTime , LocalDateTime이다.

  • LocalDate: 날짜만 표현할 때 사용한다. 년, 월, 일을 다룬다. 예) 2013-11-21
  • LocalTime: 시간만을 표현할 때 사용한다. 시, 분, 초를 다룬다. 예) 08:20:30.213
  • LocalDateTime: LocalDate 와 LocalTime 을 합한 개념이다. 예) 2013-11-21T08:20:30.213

LocalDate /  LocalTime

✔️ 생성

  • now() - 현재 날짜와 시간 반환
  • of() - 지정된 날짜와 시간으로 생성

✔️ 계산

  • plusXxx() - 특정 기간 추가 (e.g., plusDays, plusHours)

LocalDateTime

✔️ 생성

  • now() - 현재 날짜와 시간 반환
  • of() - 지정된 날짜와 시간으로 생성

✔️ 계산

  • plusXxx() - 특정 기간 추가 (e.g., plusDays, plusHours)

✔️ 분리

  • toLocalDate() - 날짜만 반환 (LocalDate)
  • toLocalTime() - 시간만 반환 (LocalTime)

✔️ 합체

  • LocalDateTime.of(localDate, localTime) - 날짜와 시간 사용해 LocalDateTime 생성

✔️ 비교

  • isBefore() - 주어진 날짜/시간 이전인지 확인
  • isAfter() - 주어진 날짜/시간 이후인지 확인
  • isEqual() - 같은 날짜/시간인지 확인하며, 객체와 타임존이 달라도 시간적으로 같으면 true를 반

ZonedDateTime

타임존 : 일광 절약 시간제에 대한 정보와 UTC+9:00와 같은 UTC로 부터 시간 차이인 오프셋 정보를 모두 포함.

ZoneId

✔️ 생성

  • ZoneId.systemDefault() -  시스템이 사용하는 기본 ZoneId 반환
  • ZoneId.of() - : 타임존을 직접 제공해서 ZoneId 를 반환

ZonedDateTime

ZonedDateTime = LocalDateTime + ZoneId(시간대 정보) 으로, 시간대를 고려한 날짜와 시간을 표현할 때 사용한다.

public class ZonedDateTime {
    private final LocalDateTime dateTime;
    private final ZoneOffset offset;
    private final ZoneId zone;
}

 

✔️ 생성

  • now() - 현재 날짜와 시간 반환, 이때 ZoneId는 현재 시스템을 따름
  • of() - 지정된 날짜와 시간으로 생성, LocalDateTime에 ZoneId 추가해서 생성

✔️ 타임존 변경

withZoneSameInstant(ZoneId) - 타임존 변경, 현재 다른 나라가 몇 시인지 확인 가능.

OffsetDateTime

OffsetDateTime = LocalDateTime + ZoneOffset (UTC 오프셋 정보)로, 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 타임존은 없고, UTC로부터의 시간대 차이인 고정된 오프셋만 포함다.

public class OffsetDateTime {
    private final LocalDateTime dateTime;
    private final ZoneOffset offset;
}

Instant

Instant란 java.time 패키지의 클래스 중 하나로, UTC 기준으로 타임스탬프를 표현하는 클래스이다. 나노초 정밀도로 1970-01-01T00:00:00Z (UTC) 기준의 시간을 나타낸다. 다시 말해 Instant 내부에는 초 데이터만 들어있기 때문에 날짜와 시간 계산에는 적합하지 않다.

 

✔️ 장점

  • UTC를 기준으로 하므로 시간대에 영향을 받지 않는다.
  • 1970년 1월 1일 UTC를 기준으로 해서 시간 계산 및 비교가 명 확하고 일관된다.

✔️ 단점

  • 사람이 읽고 이해하기에 직관적이지 않고 기능이 부족하다.
  • 특정 지역의 날짜와 시간으로 변환하 려면 추가적인 작업이 필요하다.

✔️ 사용 예시

  • 전 세계적으로 일관된 시점을 표현할 때
  • 순수하게 시간의 흐름만을 다루고 싶을 때
  • 데이터베이스에 날짜와 시간 정보를 저장하거나, 다른 시스템과 날짜와 시간 정보를 교환할 때

✔️ 생성

  • now() - 현재 UTC 시간을 반환
  • from() - 다른 타입의 날짜와 시간을 기준으로 Instant 를 생성
  • ofEpochSecond(long epochSecond) - 초 단위의 Epoch 시간으로 생성

✔️ 계산

  • plusSeconds() - 초를 더함

✔️ 조회

  • getEpochSecond() - 에포크 시간인 UTC 1970년 1월 1일 0시 0분 0초를 기준으로 흐른 초를 반환

Period

Period는 두 날짜 사이의 간격을 년, 월, 일 단위로 나타낸다.

public class Period {
    private final int years;
    private final int months;
    private final int days;
}

 

✔️ 생성

  • of() : 특정 기간을 지정해서 Period 를 생성
    • of(년, 월, 일)
    • ofDays()
    • ofMonths()
    • ofYears()

Duration

Duration은 두 시간 사이의 간격을 시, 분, 초(나노초) 단위로 나타낸다. 내부에서 초를 기반으로 시, 분, 초를 계산해 사용한다.

public class Duration {
    private final long seconds;
    private final int nanos;
}

 

✔️ 생성

  • of() : 특정 시간을 지정해서 Duration 를 생성
    • of(지정)
    • ofSeconds()
    • ofMinutes()
    • ofHours()

✔️ 시간 차이

  • between(start, end) - start와 end 사이의 시간 차이를 구함

날짜와 시간의 핵심 인터페이스

  • Temporal 인터페이스는 TemporalAccessor의 하위 인터페이스로, 날짜와 시간을 조작하기 위한 기능을 제공하는 인터페이스이다.
  • TemporalAmount 인터페이스는 시간의 간격을 나타내며, 날짜와 시간 객체에 적용하여 그 객체를 조정할 수 있는 인터페이스이다.

  • TemporalUnit, ChronoUnit - 날짜와 시간을 측정하는 단위를 나타낸다. ChronoUnit은 열거형으로 구현되어있으며, 다양한 시간 단위를 제공한다.
  • TemporalField , ChronoField - 날짜와 시간을 나타내는데 사용된다. ChronoField는 열거형으로 구현되어 있으며, 다양한 필드를 통해 날짜와 시간의 특정 부분을 나타낸다.

날짜와 시간 조작하기

✔️ TemporalAccessor : LocalDateTime 등 특정 시점의 시간 정보를 제공하는 클래스들이 구현하는 인터페이스로, 시간 정보를 읽기 전용으로 조회할 수 있도록 설계됨.

 

✔️  get(TemporalField field) 메서드

  • 특정 시간 필드(TemporalField)의 값을 반환하며, 예를 들어 ChronoField.DAY_OF_MONTH를 사용해 날짜의 "일" 조회 가능

✔️ 편의 메서드 사용

  • 자주 쓰는 시간 필드는 간결하게 사용할 수 있도록 편의 메서드를 제공하며, 예를 들어 getDayOfMonth()는get(ChronoField.DAY_OF_MONTH)와 동일한 결과를 반환
  • 편의 메서드가 있다면 이를 사용하는 것이 가독성과 유지보수에 유리
  • 특정 필드 조회에 대한 편의 메서드가 없을 경우, get(TemporalField field) 메서드를 직접 사용

✔️ Temporal

  • 시간을 조작하는 기능을 제공하며, LocalDateTime, ZonedDateTime 등이 구현
  • plus(long amountToAdd, TemporalUnit unit)은 지정된 단위(예: ChronoUnit.YEARS)로 시간을 더함
  • minus(long amountToSubtract, TemporalUnit unit)을 사용해 시간을 뺄 수도 있음

✔️ 편의 메서드 사용

  • 자주 사용하는 메서드는 편의 메서드가 제공( dt.plus(10, ChronoUnit.YEARS) dt.plusYears(10) )