Post

Java 중급 심화: 날짜/시간 API, 중첩 클래스, 예외 처리 기법

김영한의 실전 자바 중급 1편에서 학습한 내용을 정리한 글입니다.

날짜와 시간

자바 날짜와 시간 라이브러리 소개

Untitled

Local은 세계 시간대를 고려하지 않은 시간으로 국내 서비스만 고려할 때 사용된다.

LocalDateTime

LocalDateTime : Localdtae와 LocalTime을 합한 개념으로 날짜와 시간을 나타낸다.

.now() : 현재 날짜와 시간

.of() : 특정 날짜와 시간 지정

.toLocalDate() : 날짜만 분리

toLocalTime() : 시간만 분리

of(localDate, localTime) : 날짜와 시간 합체

.plusXXX( ) : 날짜 계산

.minusXXX() : 날짜 계산

.isBefore : 지정 날짜보다 이전인지 확인

.isBefore : 지정 날짜보다 이후인지 확인

.isEqual : 지정 날짜인지 확인

  • 객체가 다르고, 타임존이 달라도 시간적으로 같으면 true를 반환 (서울의 9시와 UTC의 0시는 같다고 판단)

불변이기 때문에 반환값을 사용해야한다.

ZonedDateTime

  • ZoneId.systemDefault() : 시스템이 사용하는 기본 ZoneId 를 반환한다.
    • 각 PC 환경 마다 다른 결과가 나올 수 있다.
  • ZoneId.of() : 타임존을 직접 제공해서 ZoneId 를 반환한다.

ZoneId 는 내부에 일광 절약 시간 관련 정보, UTC와의 오프셋 정보를 포함하고 있다.

ZonedDateTime: 시간대를 고려한 날짜와 시간을 표현할 때 사용한다. 여기에는 시간대를 표현하는 타임존이 포함된다.

  • now() : 현재 날짜와 시간을 기준으로 생성한다. 이때 ZoneId 는 현재 시스템을 따른다
  • of(...) : 특정 날짜와 시간을 기준으로 생성한다. ZoneId 를 추가해야 한다. LocalDateTimeZoneId 를 추가해서 생성할 수 있다.
  • withZoneSameInstant(ZoneId) : 타임존을 변경한다. 타임존에 맞추어 시간도 함께 변경된다.

ZonedDateTime 은 구체적인 지역 시간대를 다룰 때 사용하며, 일광 절약 시간을 자동으로 처리할 수 있다. 사용자 지정 시간대에 따른 시간 계산이 필요할 때 적합하다. OffsetDateTime은 UTC와의 시간 차이만을 나타낼 때 사용하며, 지역 시간대의 복잡성을 고려하지 않는다. 시간대 변환 없이 로그를 기록하고, 데이터를 저장하고 처리할 때 적합하다.

Instant

날짜와 시간을 나노초 정밀도로 표현하며, 1970년 1월 1일 0시 0분 0초(UTC 기준)를 기준으로 경과한 시간으로 계산된다.

UTC를 기준으로 하므로, 시간대에 영향을 받지 않는다. 이는 전 세계 어디서나 동일한 시점을 가리키는데 유용하다.

하지만 사람이 읽고 이해하기에는 직관적이지 않다. 예를 들어, 날짜와 시간을 계산하고 사용하는데 필요한 기능이 부족하다.

Duration, Period

Untitled

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

Untitled

TemporalAccessor 인터페이스

날짜와 시간을 읽기 위한 인터페이스

특정 시점의 날짜와 시간 정보를 읽을 수 있는 최소한의 기능을 제공한다

Temporal 인터페이스

TemporalAccessor 의 하위 인터페이스로, 날짜와 시간을 조작(추가, 빼기 등)하기 위한 기능을 제공한다.

날짜와 시간을 변경하거나 조정할 수 있다.

Untitled

TemporalUnit 인터페이스는 날짜와 시간을 측정하는 단위를 나타내며 구현체는java.time.temporal.ChronoUnit 열거형으로 구현되어있다.

ChronoUnit은 다양한 시간 단위를 제공한다.

1
2
3
4
//차이 구하기
LocalTime lt1 = LocalTime.of(1, 10, 0); LocalTime lt2 = LocalTime.of(1, 20, 0);
long secondsBetween = ChronoUnit.SECONDS.between(lt1, lt2);
System.out.println("secondsBetween = " + secondsBetween);

ChronoField 는 날짜 및 시간을 나타내는 데 사용되는 열거형이다. 주로 사용되는 구현체는 java.time.temporal.ChronoField 열거형으로 구현되어 있다.

이 열거형은 다양한 필드를 통해 날짜와 시간의 특정 부분을 나타낸다. 여기에는 연도, 월, 일, 시간, 분 등이 포함된다.

날짜와 시간 문자열 파싱과 포맷팅

  • 포맷팅 : 날짜와 시간 데이터를 원하는 포맷의 문자열로 변환 Date → String
1
2
3
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd 일");
String formattedDate = date.format(formatter); System.out.println("날짜와 시간 포맷팅: " + formattedDate);
  • 파싱 : 무자열을 날짜와 시간 데이터로 변경 String → Date
1
2
3
// 파싱: 문자를 날짜로
String input = "2030년 01월 01일";
LocalDate parsedDate = LocalDate.parse(input, formatter); System.out.println("문자열 파싱 날짜와 시간: " + parsedDate);

중첩 클래스

Untitled

정척 중첩 클래스는 정적 변수와 같이 앞에 static이 붙어있다.

내부 클래스는 인스턴스 변수와 같이 앞에 static이 붙어있지 않다.

1
2
3
4
5
6
7
8
class Outer {
     ...
//정적 중첩 클래스
static class StaticNested {
... }
//내부 클래스 class Inner {
... }
}

지역 클래스는 지역 변수와 같이 코드 블럭안에서 클래스를 정의한다.

1
2
3
4
5
6
class Outer {
public void process() { //지역 변수
int lcoalVar = 0; //지역 클래스
         class Local {...}
         Local local = new Local();
} }

익명 클래스는 지역 클래스의 특별한 버전이다.

여기서 중첩은 어떤 다른 것이 내부에 위치하거나 포함되는 구조 관계를 뜻한다.

  • 바깥과 안은 관계 없는 클래스

내부는 나의 내부에 있는 나를 구성하는 요소를 뜻한다.

  • 내부 클래스는 바깥 클래스에 소속

내부 클래스의 종류

  • 내부 클래스 : 바깥 클래스의 인스턴스 멤버에 접근
  • 지역 클래스 : 내부 클래스의 특징을 갖고 지역 변수에도 접근
  • 익명 클래스 : 지역 클래스의 특징을 갖고 클래스의 이름이 없는 특별 클래스

중첩 클래스는 언제 사용하나?

모든 중첩 클래스는 특정 클래스가 다른 하나의 클래스 안에서만 사용되거나, 둘이 아주 긴 밀하게 연결되어 있는 특별한 경우에만 사용해야 한다

중첩 클래스를 사용하는 이유

논리적 그룹화: 특정 클래스가 다른 하나의 클래스 안에서만 사용되는 경우 해당 클래스 안에 포함하는 것이 논리적으로 더 그룹화 된다. 패키지를 열었을 때 다른 곳에서 사용될 필요가 없는 중첩 클래스가 외부에 노출되지 않는장점도 있다.

캡슐화: 중첩 클래스는 바깥 클래스의 private멤버에 접근할 수 있다. 이렇게 해서 둘을 긴밀하게 연결하고 불필요한 public 메서드를 제거할 수 있다.

정척 중첩 클래스

  • 자신의 멤버에 접근이 가능하다
  • 바깥 클래스의 인스턴스 멤버에는 접근이 불가하다
  • 바깥 클래스의 클래스 멤버에는 접근이 가능하다.
  • 바깥 클래스의 private 접근 제어자에 접근 할 수 있다.

내부 클래스

  • 자신의 멤버에 접근이 가능하다
  • 바깥 클래스의 인스턴스 멤버에 접근할 수 있다
  • 바깥 클래스의 클래스 멤버에 접근할 수 있다.
  • 바깥 클래스의 private 접근 제어자에 접근할 수 있다.

지역 클래스(로컬 클래스)

내부 클래스의 특징을 그대로 가진다. 지역 변수와 같이 코드 블럭 안에서 정의 된다.

1
2
3
4
5
6
7
8
9
10
11
class Outer {
public void process() { 
		//지역 변수
		int localVar = 0;
		
		 //지역 클래스
     class Local {...}
     Local local = new Local();
	} 
}

단, 지역 변수처럼 접근 제어자를 사용할 수 없고 지역 클래스가 접근하는 지역 변수의 값은 변경하면 안된다.

변수 캡처

지역 변수의 생명주기는 짧고, 지역 클래스를 통해 생성한 인스턴스의 생명 주기는 길다. 지역 클래스를 통해 생성한 인스턴스가 지역 변수에 접근해야 하는데, 둘의 생명 주기가 다르기 때문에 인스턴스는 살아 있지만, 지역 변수는 이미 제거된 상태일 수 있다. 이런 문제를 해결하기 위해 자바에서는 인스턴스를 생성하는 시점에 필요한 지역 변수를 복사해서 인스턴스에 함께 넣어둔다.

Untitled

지역 클래스가 접근하는 지역 변수는 절대로 중간에 값이 변하면 안되기 때문에 final로 사용하거나 사실상 fianl 이어야 한다.

  • 사실상 fianl
    • 지역 변수에 final 키워드를 사용하지는 않지만 값을 변경하지 않는 지역 변수

캡처 변수의 값을 변경하지 못하는 이유

  • 지역 변수의 값을 변경하면 인스턴스에 캡처한 변수의 값도 변경이 필요하다
  • 반대의 경우에도 변경이 필요하다
  • 즉, 서로 동기화가 필요한데 멀티 쓰레드 상황에서 동기화는 매우 어려운 문제이고 성능에 영향을 줄 수 있다
  • 따라서 변경하지 못하도록 하여 근본적으로 문제를 차단한다

필요하면 변수를 선언해서 대입 후 변경하여 사용한다.

익명 클래스

특징

  • 이름 없는 지역 클래스를 선언하면서 동시에 생성한다
  • 부모 크래스를 상속 받거나 인터페이스를 구현해야한다.
  • 이름을 가지지않으므로 생성자를 가질 수 없다. 기본 생성자만 사용된다.
  • 바깥클래스이름$1 과 같이 이름이 정의된다.

장점

클래스를 별도로 정의하지 않고도 인터페이스나 추상 클래스를 즉석에서 구현할 수 있어 코드가 간결하다. 하지만 복잡하거나 재사용이 필요하면 별도의 클래스를 정의하는 것이 좋다

1
2
3
4
5
6
7
8
9
 class Dice implements Process {
             @Override
             public void run() {
                 int randomValue = new Random().nextInt(6) + 1;
System.out.println("주사위 = " + randomValue); 
					}
 }

hello(dice);

함수를 전달할 수 없으니 인스턴스를 전달하고 인스턴스의 메소드를 실행하여 재사용성을 높힐 수 있다. 따라서 인터페이스를 선언하고 구현체를 각 상황에 맞게 전달한다.

또는 익명 클래스를 직접 전달할 수 있다.

1
2
3
4
5
6
7
8
hello(new Process() {
             @Override
             public void run() {
                 for (int i = 1; i <= 3; i++) {
                     System.out.println("i = " + i);
                 }
 }
 });

자바8에 들어서면서 함수를 인수로 전달할 수 있게 되었는데 이를 람다라고 한다

1
2
3
4
  hello(() -> {
	  int randomValue = new Random().nextInt(6) + 1;
		System.out.println("주사위 = " + randomValue); 
});

예외처리

외부 서버와 통신할 때는 다양한 문제들이 발생한다

  • 네트워크 오류
  • 데이터 전송 문제 실패

오류가 발생하면 데이터를 전송하지 않아야 하고 오류에 관련된 로그를 자세히 남겨야 한다

Tip 부정은 메소드 추출로 변환하여 읽기 좋게한다

1
2
if(!connectResult.equals("success"))

1
2
3
4
5
6
7
8
if(isError(connectResult)) {

}

private static boolean isError(String connectResult) {
    return !connectResult.equals("success");
}

Tip : 외부연결과 같은 자바 외부의 자원은 자동으로 해제가 안되기 때문에 외부 자원을 반드시 반납해야한다

1
2
3
4
5
6
7
8
9
10
  String connectResult = networkClient.connect();
  if(isError(connectResult)) {
      System.out.println("[네트워크 오류 발생] 오류 코드 : " + connectResult);
  } else {
      String sendResult = networkClient.send(data);
      if(isError(sendResult)) {
          System.out.println("[네트워크 오류 발생] 오류 코드 : " + sendResult);
      }
  }
  networkClient.disconnect();

NetworkClient 사용 시 주의사항

  • connect가 실패한 경우 send()를 호출하면 안된다
  • 사용 후 반드시 disconnect()를 호출하여 외부 연결을 해제한다
    • connect, send 호출에 오류가 있어도 disconnet를 반드시 호출한다

단, 반환 값으로 예외를 처리하는 부분은 정상 흐름과 예외 흐름이 전혀 분리되어 있지 않아 코드의 이해가 어렵다. 이런 문제를 해결하기 위해 예외 처리 메커니즘이 존재한다

자바 예외처리 - 예외 계층

Untitled

  • Object : 예외의 최상위 부모도 Object
  • Throwable : 최상위 예외로 하위에 Exception과 Error가 있다
  • Error : 메모리 부족이나 시스템 오류와 같이 어플리케이션에서 복구가 불가능한 예외이다. 애플리케이션 개발자는 이 예외를 잡으려고 하면 안된다
  • Exception : 체크 예외
    • 애플리케이션 로직에서 사용할 수 있는 실질적 최상위 예외이다
    • Exception과 그 하위 예외는 컴파일러가 체크하는 체크 예외이다. 단, RuntimeException은 예외로한다
  • RuntimeException : 언체크 예외(런타임 예외)
    • 컴파일러가 체크하지 않는 언체크 예외이다.
    • RuntimeException과 하위 예외는 모두 언체크 예외이다.

체크 예외는 발생한 예외를 개발자가 명시적으로 처리해야한다. 그렇지 않으면 컴파일 오류가 발생한다. 언체크 예외는 개발자가 발생한 예외를 명시적으로 처리하지 않아도 된다

상위 예외를 catch로 잡으면 하위 예외까지 잡을 수 있다. 따라서 어플리케이션 로직에서는 Throwable 예외를 잡으면 안된다. 그 이유는 Error 예외도 잡기 때문이다.

예외 기본 규칙

  1. 예외가 발생하면 잡아서 처리하거나 밖으로 던져야한다

Untitled

  1. 예외를 잡거나 던질 때 지정한 예외뿐만 아니라 하위 자식들도 함께 처리할 수 있다

자바 main 밖으로 예외를 던지면 예외 로그를 출력하면서 시스템이 종료된다

체크 예외

체크 예외는 잡아서 처리하거나 밖으로 던진다. 그렇지 않으면 컴파일 오류가 발생한다

Exception을 상속한 예외는 체크 예외가 된다

1
2
3
4
5
6
7
public class MyCheckedException extends Exception{

    public MyCheckedException(String message) {
				//부모에 에러메시지를 보관한다
        super(message); 
    }
}

예외를 던질려면 예외 객체를 생성하고 밖으로 던질 수 있는 throws 키워드를 사용해야한다

1
2
3
4
5
6
public class Client {
    public void call() throws MyCheckedException {

        throw new MyCheckedException("ex");
    }
}

예외를 받는 쪽에서는 try catch로 예외를 핸들링 할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Service {
    Client client = new Client();

    public void callCatch() {
        try {
            client.call();
        } catch (MyCheckedException e) {
            System.out.println("예외 처리 " + e.getMessage());
        }
        System.out.println("정상흐름");
    }
}

장점

  • 실수로 예외를 누락하지 않도록 해준다. 이를 통해 어떤 체크 예외가 발생하는지 쉽게 파악할 수 있다

단점

  • 번거롭다. 크게 신경쓰고 싶지 않은 예외까지 챙겨야한다

언체크 예외

예외를 잡아서 처리하지 않아도 된다

1
2
3
4
5
6
public class MyUncheckedException extends RuntimeException{
    public MyUncheckedException(String message){
       super(message);
    }
}

1
2
3
4
5
6
public class Client {
    public void call() {
        throw new MyUncheckedException("ex");
    }
}

언체크 예외도 throws 예외를 선언해도 되지만 생략도 가능하다. 중요한 예외인 경우 적어두면 IDE를 통해 빠르게 확인을 할 수 있다

장점

  • 신경쓰고 싶지 않은 예외를 무시 할 수 있다

단점

  • 개발자가 실수로 예외를 누락할 수 있다

자바는 어떤 경우라도 반드시 호출되는 finally 기능을 제공한다 try-catch 안에서 잡을 수 없는 예외가 발생해도 finally는 반드시 호출되고 예외가 밖으로 던져진다

예외 계층

Untitled

예외를 계층화하면 한번에 처리하거나 특정 예외를 처리할 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
public class sendExceptionV3 extends NetworkClientExceptionV3{
    private final String sendData;

    public sendExceptionV3(String message, String address) {
        super(message);
        this.sendData = address;
    }

    public String getSendData(){
        return sendData;
    }
}

1
2
3
4
5
6
public class NetworkClientExceptionV3 extends Exception{
    public NetworkClientExceptionV3(String message) {
        super(message);
    }
}

이제 오류 코드로 구분하는 것이 아니라 에러 객체로 분류가 가능해졌다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    public void connect() throws ConnectExceptionV3 {
        if (connectError) {
            throw new ConnectExceptionV3(address, address + " 서버 연결 실패");
        }
        System.out.println(address + " 연결 성공 ");
    }

    public void send(String data) throws sendExceptionV3 {
        if (sendError) {
            throw new sendExceptionV3(data, address + " 서버에 데이터 전송 실패 : " + data);
        }

        System.out.println(address + " 서버에 전송 성공 : " + data);
    }
1
2
3
4
5
6
7
8
9
10
try {
      networkClient.connect();
      networkClient.send(data);
    } catch (ConnectExceptionV3 e) {
        System.out.println("[연결 오류] 오류 주소 :" + e.getAddress() + "오류 메시지 : " + e.getMessage());
    } catch (SendExceptionV3 e){
        System.out.println("[연결 오류] 오류 데이터 :" + e.getSendData() + "오류 메시지 : " + e.getMessage());
    } finally {
        networkClient.disconnect();
    }

위와 같은 구조는 에러 유형을 모두 catch로 잡는 것은 번거롭다.

중요 예외와 그 외로 분류하여 계층을 간소화 할 수 있다

  1. 연결 오류
  2. 네트워크 오류
  3. 알 수 없는 오류
1
2
3
4
5
6
7
8
9
10
11
12
try {
      networkClient.connect();
      networkClient.send(data);
    } catch (ConnectExceptionV3 e) {
        System.out.println("[연결 오류] 오류 주소 :" + e.getAddress() + "오류 메시지 : " + e.getMessage());
    } catch (NetworkClientExceptionV3 e) {
        System.out.println("[네트워크 오류] 오류 메시지 : " + e.getMessage());
    } catch (Exception e) {
        System.out.println("[알 수 없는 오류] 오류 메시지 : " + e.getMessage());
    } finally {
        networkClient.disconnect();
    }

여기서 순서는 하위부터 작성해주어야 한다 (부모가 하단에 위치해야한다)

또는 여러 종류의 예외를 하나의 catch에서 잡으려면 | 를 사용할 수 있다

실무에서의 예외 처리

처리 할 수 없는 예외 (상대 네트워크 서버 통신 불능 또는 데이터베이스 서버 문제 등)은 예외를 잡아도 해결 할 수 있는 것이 거의 없다. 이럴 경우엔 고객에게는 “현재 시스템에 문제가 있습니다” 라는 오류 메시지나 웹의 경우 오류 페이지를 보여준다. 내부 개발자가 문제 상황을 빠르게 인지할 수 있도록 오류에 대한 로그를 남겨둬야 한다

체크 예외의 부담

예외가 많아지고 복잡해지면서 체크 예외를 사용하는 것이 점점 부담스러워졌다

Untitled

최악의 수는 Exception을 던지는 것이다. 이렇게 던지는 경우 다른 체크 예외를 잡을 수가 없다. 즉 주요 체크 예외를 다 놓치게 된다.

1
2
3
4
5
6
class Facade {
     void send() throws Exception
}
 class Service {
     void sendMessage(String data) throws Exception
}

그러므로 본인이 해결할 수 있는 예외만 잡아서 처리하고, 본인이 해결할 수 없는 예외는 신경쓰지 않는 것이 더 나은 선택일 수 있다.

처리할 수 없는 예외들은 중간에 여러곳에서 나누어 처리하기 보다는 예외를 공통으로 처리할 수 있는 곳을 만들 어서 한 곳에서 해결하면 된다.

언체크 예외 사용 시나리오

Untitled

위 예외들은 잡아도 애플리케이션에서 할 것이 없다. 프로그램이 종료되지 않도록 메인에서 try-catch를 사용하고 에러 핸들러에서는 사용자 안내문과 디버깅용 스택트레이스를 남겨둔다. 또한 공통 예외처리 부분에서 예외의 타입을 확인하여 별도의 처리도 가능하다

1
2
3
4
5
try {
      networkService.sendMessage(input);
  } catch (Exception e){
      exceptionHandler(e);
  }
1
2
3
4
5
    private static void exceptionHandler(Exception e) {
        System.out.println("사용자 메시지: 죄송합니다. 알 수 없는 문제가 발생했습니다.");
        System.out.println("디버그 메시지");
        e.printStackTrace(System.out);
    }

try-with-resources

try가 끝나면 외부 자원을 반납하는 패턴이 반복되면서 자바 7에서는 try with resources라는 편의 기능을 도입하였다. 이 기능을 사용하려면 AutoCloseable 인터페이스를 구현해야한다. 이 인터페이스를 구현하면 try with resource를 사용할 때 try가 끝나는 시점에 clsoe()가 자동으로 호출된다.

AutoCloseable 인터페이스

1
2
3
4
 public interface AutoCloseable {
     void close() throws Exception;
}

AutoCloseable의 close 함수를 구현한다

1
2
3
4
5
6
7
8
public class NetworkClientV5 implements AutoCloseable{

...
    @Override
    public void close() throws Exception {
        disconnect();
    }
}

try가 끝나는 순간 자동으로 구현한 AutoCloseable의 close를 호출한다. 그 뒤에 catch를 실행한다.

1
2
3
try (Resource resource = new Resource()) { // 리소스를 사용하는 코드

}

장점

  • 리소스 누수 방지 : finally를 누락해도 모든 리소스가 제대로 해제되도록 예방할 수 있다
  • 코드 간결성 : 명시적 close 호출이 필요 없다. 더 간결하다.
  • 스코프 범위 한정 : try 블럭안에서 사용하는 리소스의 범위를 한정 할 수 있다
  • 더 빠른 자원 해제 : fianlly는 catch 이후에 자원을 반납하지만 with-resource는 catch 전에 자원을 반납한다
This post is licensed under CC BY 4.0 by the author.