프로세스와 쓰레드

프로세스란 실행중인 프로그램이라고 이해하면 쉽다.

이를 실행하면 OS(운영체제)로 부터 실행에 필요한 자원을 할당받아 프로세스가 된다.

이러한 프로세스쓰레드로 구성이 되어있다.

쓰레드란 프로세서의 자원을 이용해서 실제로 작업을 수행하는 것이다.

으러한 쓰레드가 둘 이상으로 늘어나면 이를 멀티쓰레드 프로세스라고 한다.

멀티태스킹과 멀티쓰레딩

멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.

정말 쉬운 예제가 있다.

image

이런 코어가 사람이라고 생각하고 쓰레드가 손이라고 생각한다면

코어는 단 하나의 작업만 할 수 있다. 왜냐하면 사람 혼자니까

하지만 손을 여러개를 사용하여 금방 문제를 끝낼 수 있다. 라고 생각하면 된다.

그러면 쓰레드가많아지면 그만큼 성능이 좋다? 라고는 꼭 말할 수 는없다.

멀티쓰레딩의 장단점

멀티쓰레딩의 장점

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

예를들어 멀티쓰레딩을 못한다면 다음과 같은 상황이 벌어진다.

내가 유튜브를 보면서 친구와 카톡을 하지 못하는 것이다. 오직 한가지만 사용을 해야한다.

그래서 이런 서버 프로그램들은 서버 프로세스를 여러가지 생성하는 것 보다 쓰레드를 생성을 하는 것이 좀더 시간 적인 측면과 메모리적인 측면에서 이득을 볼 수 있다.

이러면 멀티쓰레딩만 쓰면 무조건 좋겠네? 라고 생각하는데 그렇지만은 않다.

멀티쓰레딩을 사용하기 위해서는 synchronization, deadlock 같은 문제점이 발생하기 때문에 신중히 사용해야한다.

쓰레드의 구현과 실행

쓰레드를 사용하기 위해서는 2가지 방법이 있다.

  1. Thread클래스를 상속받는 법
  2. Runnable인터페이스를 구현하는 방법

보통은 자바에서는 단일상속을 하기 때문에 만약 Thread클래스를 상속받아버리면

다른 클래스들을 상속받을 기회가 사라지기 때문에 2번의 방법을 선호한다.

Runnable()인터페이스는 다음과 같이 run()만 정의 되어있는 간단한 인터페이스이다.

class MyThread implements Runnable {
    public void run() { /... }
}

이러한 Thread클래스를 상속받아 사용할 때와 Runnable인터페이스를 구현한 경우의 인스턴스 생성방법은 다르다.

// Thread의 인스턴스 생성방법 (1)
ThreadEx t1 = new TreadEx();

// Runnable 인스턴스 생성방법 (1)
Runnable r = new ThreadEx();
Thread t2 = new Thread(r);

// Runnable 인스턴스 생성방법 (2)
Thread t2 = new Thread(new ThreadEx()); // 2줄이아닌 한번에 생성하는 법

또한, 둘의 차이점은 currentThread()에도 있다. 이는 현재 실행중인 쓰레드의 참조를 반환한느 메서드이다.

만약 Thread클래스에서 상속받았다면 부모인 Thread클래스의 메서드를 직접 호출이 가능하지만

Runnable인터페이스에서 상속받았다면 사용하기 위해서 Thread클래스의 static메서드인 currentThread()를 호출하는 방법으로 사용할 수 있다.

다음은 이에대한 예시이다.

// Thread클래스를 상속받았을 때
class ThreadEx extends Thread{
    @Override
    public void run() {
        System.out.println(getName());
    }
}

//Runnalbe 인터페이스를 사용할 때
class ThreadRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

다음 같이 Runnable인터페이스를 사용한 run()메서드는 Thread클래스를 사용해 static함수를 사용하여 불러오는 것을 볼 수 있다.

쓰레드의 실행

쓰레드를 생성해도 start()메서드를 호출하지 않으면 실행이 되지않는다.

그렇다고 start()메서드를 사용했다고 바로 실행되는 것이 아니라

실행 대기 상태에 있다가 자신의 차례일 때 실행이 된다.

이러한 실행순서는 OS에서 작성한 스케쥴에 의해 실행이 된다.

또한, 한 번 실행이 종료된 쓰레드는 다시실행 할 수없다는 점이 특징이다.

그래서 만약 start()메서드를 한 쓰레드에 대해 2번실행 한다면 IllegalThreadStateException이 발생하게 된다.

start()와 run()

이러한 start()run()메서드의 의미는 비슷한것처럼 느껴지지만 다르다.

일단 run()은 생성된 쓰레드를 실행시키는 것이 아닌 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.

그렇지만 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해 생성된 호출에 run()이 첫 번째로 올라가게 한다.

이러한 호출스택은 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸된다.

정리하자면 다음과 같이 실행이 된다.

  1. main메서드에서 쓰레드의 start()를 호출한다
  2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
  3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
  4. 호출스택은 이제 2개이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.

우리가 사용하는 main메서드도 작업을 수행하는 쓰레이드이다. 그래서 프로그램을 실행하면 우리도 모르게 하나의 쓰레드를 생성하여 main메서드를 수행한다.

싱글쓰레드와 멀티쓰레드

싱글쓰레드는 2개의 작업을 실행할 때 작업이 한 작업을 끝마치고 나서야 다른 작업을 처리할 수 있다.

하지만 멀티쓰레드는 쓰레드가 번갈아가면서 작업을 한다. 이 작업이 매우 짧은 시간이라 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.

하지만 이런것이 멀티쓰레드가 꼭 빠르다고 볼 수는 없다. 왜냐하면 번갈아가면서 작업을 하기 때문에 쓰레드간 작업 전환에 시간이 더 걸리기 때문에 더 많은 시간이 소요된다.

또는, 한 쓰레드가 작업을 하고 있는 동안 다른 쓰레드는 작업이 끝나기를 기다려야하는데 이때 발생하는 대기시간 때문에 그렇다.

결론적으로는, 하나의 자원을 사용하는 작업에 대해서는 싱글쓰레드가 효율적이다. 라고 말할 수 있다.

이러한, 멀티쓰레드로 진행한 작업은 실행할 때마다 다른 결과를 얻을 수 있다.

이유는 바로 프로세스가 OS의 프로세스 스케줄러의 영향을 받기 때문이다.

하지만 여러 개의 작업을 사용하는 작업일 떄는 멀티 쓰레드가 더 효율 적이다.

예를들어 메세지를 사용한다고 해보자

여기서의 작업은 2가지가 있다.

  1. 사용자의 입력을 기다리는 것
  2. 사람들의 카톡을 받는것

이거는 그냥 예시인것으로 기억하자!! 실제로는 아니다.

싱글쓰레드인 경우는 사용자가 입력을 할때 입력하는 작업에 집중해야 하기 때문에 다른사람의 카톡을 읽어오는 작업을 할 수 가 없다.

왜냐하면 싱글쓰레드는 한가지의 작업이 끝난 뒤 다음 작업을 할 수 있기 때문이다.

하지만, 만약에 멀티쓰레드라면 사용자의 입력을 하는 도중 다른 쓰레드가 입력을 기다릴 때

다른 사람의 메시지도 가져오는 것이다.

결론적으로, 여러개의 자원을 사용하는 작업에 대해서는 멀티쓰레드가 효율적이다.

다음 예시를 보자, 다음 예시는 2가지의 작업이 있다.

  1. 사용자의 입력을 받는 작업
  2. 10~1까지 출력하는 작업이 있다.

씽글쓰레드라면 다음과 같이 작동한다.

싱글쓰레드

//싱글 쓰레드 일때
public class temp {
    public static void main(String[] args) {

        String input = JOptionPane.showInputDialog("값을 입력해 주세요.");
        System.out.println("입력한 값은" + input + "입니다.");

        for(int i = 10 ; i > 0 ; i--){
            System.out.println(i);
            try{
                Thread.sleep(1000);
            }catch (Exception e) {}
        }

    }
}

멀티쓰레드 일때는 다음과 같다.

//멀티 쓰레드 일 때
public class temp {
    public static void main(String[] args) {

       ThreadEx th1 = new ThreadEx();
       th1.start();

       String input = JOptionPane.showInputDialog("값을 입력해 주세요");
        System.out.println("입력한 값은" + input + "입니다.");

    }
}

class ThreadEx extends Thread{
    @Override
    public void run() {
        for(int i = 10 ; i > 0 ; i--){
            System.out.println(i);
            try{
                sleep(1000);
            } catch (Exception e) {}
        }
    }
}

멀티쓰레드

쓰레드의 우선순위

쓰레드는 우선순위(priority)라는 속성을 가지고 있다.

이러한 우선순위의 순서에 따라서 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수도 있고

그렇기 때문에 사용자에게 빠르게 반응해야하는 특정작업들의 우선순위를 높여야 한다.

쓰레드의 우선순위 지정하기

쓰레드의 우선순위를 변경하는 메서드들은 다음과 같다.

void setPriority(int newPriority) // 쓰레드의 우선순위를 변경하는 메서드
int getPriority() // 쓰레드의 우선순위를 반환하는 메서드

// 전역변수
public static final int MAX_PRIORITY = 10 // 최대 우선순위
public static final int MIN_PRIORITY = 1 // 최소 우선순위
public static final int NORM_PRIORITY = 5 // 보통 우선순위

쓰레드의 우선순위는 특징이 있다.

  • 우선순위는 1~10이고 이는 숫자가 높을수록 우선순위가 높다
  • 쓰레드의 우선순위는 쓰레드를 실행하기 전에만 변경이 가능하다.
  • 멀티코어에서는 쓰레드의 우선순위에 따른 차이가 거의 없다.
    • 멀티코어에서는 쓰레드에 높은 우선순위를 주면 더 많은 실행시간과 실행기회를 갖게 될 것이라고 기대할 수 없다.
    • 빨리 끝나도 나머지 작업을 끝내야하기 때문에 그렇다. 시간이 비슷하다.
  • 어떤 OS에서 실행하느냐에 따라 스케줄러가 다르기 때문에 다른 결과를 얻을 수 있다.

쓰레드 그룹

쓰레드 그룹이란 서로 관련된 쓰레드를 그룹으로 다루기 위한 것이다.

이러한 쓰레드 그룹은 보안상의 이유로 도입된 개념이다.

쓰레드 그룹에 포합시키기 위해서는 Thread의 생성자를 이용해야한다.

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.

그렇기 때문에 만약 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는

기본적으로 자신을 생성한 쓰레드와 같은 그룹에 속하게 된다.

자바어플리케이션이 실행될때 JVM은 쓰레드 그룹을 생성한다.

JVMmainsystem이라는 쓰레드 그룹을 생성한다

이후 필요한 쓰레들을 생성해서 mainsystem쓰레드 그룹에 분류한다.

만약, 쓰레드그룹을 지정하지 않는다면 보통은 main쓰레드 그룹에 속하게 된다.

이러한 그룹에 관한 메서드는 다음과 같다.

ThreadGroup getThreadGroup() // 자신이 속한 쓰레드 그룹을 반환한다.
void uncaughtException(Thread t, Throwble e) // 쓰레드 그룹의 쓰레드가 처리되지 않은
//예외에 의해 실행이종료될 때, JVM에 의해 자동적으로 이 메서드가 호출된다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

지네릭스

지네릭스란 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.

예를 들어 ArrayList에서 만약 형을 정해주지 않으면 뜻하지 않은 형이 나타날 수도 있다.

나는 Integer만 저장하고 싶은데 String이 저장될 수 있고 이러한 것을 방지하고 또한, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄이기 위해 사용한다.

즉, 다시 말하자면 지네릭스의 장점은 이거라 할 수 있다.

  • 타입 안정성을 제공한다.
  • 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

class Box<T>{
    T item;

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

이와 같이 본다면 C++에서 Template과 비슷한 점을 볼 수 있다.

또한, 이렇게 선언한 T를 타입변수라고 한다. 꼭 T만 사용할 수 있는것이 아니라 필요에 따라서 다른 문자를 사용해도된다.

이렇게 선언을 했다면 객체를 생성할 때 <T>를 붙여줘야한다.

그렇다고 안붙여주면 컴파일이 안되는 것은 아니고 경고가 발생한다.

Box b = new Box(); // 타입변수를 사용하지 않아 Object로 자동조정
b.setItem("Hello"); // 경고 발생 unchecked or unsafe operation

Box<String> b = new Box<T>();
b.setItem("Item"); // 경고발생 x 

또한, 다른타입으로 생성이 불가능하다.

Box<Apple> appleBox = new Box<Grape>(); // 선언하는 타입과 생성자에 대입된 타입이 일치 x

그러면 상속관계에서는 괜찮지 않을까 하는데 경기도 오산이다.

FruitApple이 상속했다고해도 생성이 불가능하다.

하지만 대입된 타입이 같은 경우는 가능하다.

Box<Fruit> box = new Box<Apple>(); //불가능

//대입된 타입이 같은경우는 가능
Box<Apple> box = new FruitBox<Apple>();

또한, 당연히 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

하지만, 상속관계에서는 가능하다. 객체 생성이 불가능 한거지 매개변수는 가능하다.

//대입된 타입과 다른 타입의 객체 추가 x
Box<Apple> box = new Box<Apple>();
box.add(new Apple()); // 가능
box.add(new Grape()); // 불가능

// 상속관계에서 가능 예시
Box<Fruit> box = new Box<Fruit>();
box.add(new Fruit());
box.add(new Apple());

지네릭스의 용어정리

예를들어 Box<T>라는 지네릭 클래스가 존자한다고 한다.

Box<T>지네릭 클래스라고 불린다 또한, T타입 변수 or 타입 매개변수라고 불린다.

Box원시 타입으로 불린다.

지네릭스의 제한

지네릭스는 제한되는 점이 있다.

  1. static멤버에 타입 변수를 사용할 수 없다.

만약 static멤버에 타입 변수를 사용한다면 인스턴스변수로 간주된기 떄문이다.

static멤버는 인스턴스 변수를 참조할 수 없기 때문이다.

  1. 지네릭타입의 배열을 선언할 수 없다.

왜냐하면 new연산자 떄문이다. new는 컴파일 하는 시점에는 T가 어떤타입인지 알아야하지만 컴파일 하는 시점에서는 T가 어떤타입인지 알 수 없기 때문이다.

이러한 지네릭 클래스는 타입변수 T가 제한이 되있지 않아서 아무 타입종류나 다 들어갈 수 있다.

이러한 타입변수를 제한할 수 있는 방법은 없을까?

해결방법은 extends를 사용하면된다. 이를 사용하면 특정 타입의 자식들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit>{ // Fruit의 자식만 타입으로 지정할 수 있다.
    /...
}

또한, 클래스가아닌 인터페이스도 포함한다는 제약이 필요하다면 implements가 아닌 그냥 extends를 사용해서 해결하면 된다. <T extends Eatable> 이런식으로 사용하면 된다.

만약, 클래스의 자손이면서 인터페이스도 구현하려면 &를 붙여주면 해결된다. <T extends Eatable & Fruit> 이렇게 하면 Fruit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수에 대입될 수 있다.

와일드 카드

static메서드에서는 타입 매개변수를 매개변수로 사용할 수 없기 때문에 도입하였다.

class Juicer {
  static Juice makeJuice(FruitBox<Fruit> box) {
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);
  }
}

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();

Juicer.makeJuice(fruitBox); // 가능하다.
Juicer.makeJuice(appleBox); // 불가능하다. 형변환을 할 수없기 때문이다.

이를 해결하면 그냥 makeJuice라는 메소드를 오버로딩하면 되지 않겠냐 생각할 수도 있지만 이것도 경기도 오산이다.

왜냐하면 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문이다.

그래서 Apple타입을 오버로딩을 함부로 할 수 없다.

이를 해결하기 위해서 나온것이 바로 와일드 카드이다.

사용하는법은 다음과 같다.

  • <? extends T> : 와일드 카드의 상한 제한, T 와 그 자식들만 가능
  • <? super T> : 와일드 카드의 하한 제한, T 와 그 부모들만 가능
  • <?> : 제한 없음. 모든타입이 가능하다. == <? extends Object>와 동일하다

또한, 특징이 있는데 와일드 카드에서는 &를 사용할 수 없다는 것이 특징이다.

지네릭 메서드

지네릭 메서드는 메서드의 선언부에 지네릭 타입이 선언된 메서드를 말한다.

static <T> void sort<List<T> list, Comparator<? super T> c)

지네릭 메서드는 굳이 지네릭 클래스가 아닌 클래스에서도 정의될 수가 있다.

그리고 static멤버와 차이점이 있는데 staic멤버는 타입 매개변수를 사용할 수 없지만 지네릭 타입을 메서드에 선언하고 사용하면 가능하다.

또한, 타입 매개변수는 메서드 내에서만 지역적으로 사용될 것이므로 static이건 아니건 상관이 없다.

예시로 makeJuice()를 지네릭 메서드로 바꾸면 다음과 같이 변경된다.

static Juice makeJuice (FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

// 지네릭 메서드로 변환
static <T extends Fruit> Juice makeJuice (FruitBox<T> box){
    String tmp = "";
    for(Fruit f : box.getList()) tmp += f + " ";
    return new Juice(tmp);
}

이러한 메서드를 호출할 떄는 반드시 타입변수에 타입을 대입하고 호출해야한다.

지네릭 타입의 형변환

지네릭타입은 non지네릭 타입간의 형변환은 항상 가능하다. 하지만 경고가 발생하긴한다.

Box<Object> objBox = null;
Box box = null;

box = (Box)objBox; // 가능 -> 경고발생
objBox = (Box<String>)box; //가능 -> 경고발생

하지만 대입된 타입이 다른 지네릭타입간의 형변환은 불가능하다.

Box<Object> objBox = null;
Box<String> strBox = null;

objBox = (Box<Object>)strBox; //에러발생 
strBox = (Box<String>)objBox; // 에러발생

만약 와일드카드 지네릭 타입에서 지네릭타입으로의 형변환을 어떨까?

결론적으로말하면 서로 형변환이 가능하다.

또한, 와일드카드 지네릭 타입에서 와일드카드 지네릭 타입으로의 형변환도 가능하다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 쓰레드 (1)  (0) 2021.09.24
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

DateCalendar가 가진 문제점을 해결하기 위해서 JDK.18부터 java.time패키지가 추가되었다.

이안에는 총 4개의 하위 패키지로 구성되어 있다.

  • java.time
    • 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
  • java.timechrono
    • 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
  • java.time.temporal
    • 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공
  • java.time.zone
    • 시간대와 관련된 클래스들을 제공

Java.time패키지의 핵심 클래스

java.time패키지의 특징으로는 날짜와 시간을 별도의 클래스로 분리해 놓았다.

그래서 다음과 같이 사용한다.

  • 시간 : LocalTime클래스
    • 시간 - 시간 (기간) : Duration클래스
  • 날짜 : LocalDate클래스
    • 날짜 - 날짜 (기간) : Period클래스
  • 시간 + 날짜 : LocalDateTime클래스
  • 시간대 + 시간 + 날짜 : ZonedDateTime

이러한 클래스들을 사용하기 위해 객체를 생성해야하는데 생성하는 법은 다음과 같다.

LocalDate date = LocalDate.now(); // new를 사용하지 않음

LocalDate date = LocalDate.of(2021,09,18);

이런식으로 now()of()를 사용해서 객체를 생성할 수 있다. LocalDate를 예로 들었지만 다른 클래스들도 이런방식으로 생성하면 된다.

이러한 now()of()static메서드이다.

인터페이스의 종류

Temporal과 TemporalAmount

Temporal은 보통 날짜와 시간을 위한것이라고 생각하면 좋다.

  • Temporal, TemporalAccessor, TemporalField 를 구현한 클래스
    • LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant ...
  • TemporalAmount를 구현한 클래스
    • Period, Duration

TemporalUnit과 TemporalField

  • TemporalUnit: 날짜와 시간의 단위를 정의해 놓은 인터페이스, 열거형 ChronoUnit으로 구성되어 있다.
  • TemporalField : 년, 월, 일 등 날짜와 시간의 필드를 정의해 놓은 것, 이것도 열거형 ChronoField가 이 인터페이스를 구성하였다.

다음과 같은 예시를 보면 이해가 빠르다. 다음 예시는 특정 필드의 값을 얻기위한 매서드인 get()과 날짜와 시간을 빼거나 더하는 plus()에 대한 정의 이다.

int get(TemporalField field)
LocalDate plus(long amountToAdd, TemporalUnit, unit);

이를 TemporalField인지 TemporalUnit에서 사용할 수 있는 열거형인지 판단하는 방법은 다음과 같다.

boolean isSupported(TemporalField field);
boolean isSupported(TemporalUnit unit);

LocalDate와 LocalTime

LocalDateLocalTime은 날짜와 시간을 나타낸다.

이러한 값들을 생성하는 of()now()메서드는 static메서드로 특히 of()에는 다양한 매개변수가 오버로딩 되어있다.

LocalTime을 예로 들면 시간, 분, 초 뿐만이 아니라 나노초도 포함이 된다.

또한, 이들의 특징으로는 LocalTime을 예로들자면 만 단위로 지정해서 넣어줄 수 도 있다.

LocalTime time = LocalTime.ofSecondDay(86399); // 23시 59분 59초

또한, 시간과 표를 나타내는 것을 여러가지 방법이 있을 텐데 parse()메서드를 통해서 문자열을 날짜로 변환할 수 있다.

LocalDate date = LocalDate.parse("1999-03-16"); // 1999년 3월 16일
LocalTime time = LocalTime.parse("23:59:59); // 23시 59분 59초

get메서드

보통 Java에서는 특정 필드를 가져올 때는 get() 특정필드를 수정하거나 값을 변경할 때는 set()을 사용한다.

물론 LocalDateLocalTime에도 각 시간, 분, 초 이러한 단위들을 가져오는 get()함수들이 다량 존재한다.

인텔리제이나, 이클립스같은 IDE의 기능을 사용하여 살펴보자.

몇개만 소개하자면 다음과 같다.

LocalDate date = LocalDate.now();

date.getYear() // int형 년도 반환
date.getMonthValue() // Month형 월 반환
date.getMonth() // int형 월 반환
....

LocalTime time = LocalTime.now();

time.getHour()// int형 시간 반환
time.getMinute() // int형 분 반환
time.getSecond() // int형 초 반환
time.getNano() // int형 나노초 반환

지금은 get()만 사용했지만 기간이 너무 많거나 int형으로 표현이 안되는 값들은 getLong()을 사용하여 반환받는 것이 맞다.

ChronoField

이러한 get()을 하려할 때 메서드들의 매개변수로 사용할 수 있는 필드의 목록이다.

이는의 예시들을 다음과 같이 있다.

ERA // 시대
YEAR_OF_ERA , YEAR // 년
DAY_OF_WEEK // 요일 (1: 월요일, 2: 화요일)

MICRO_OF_SECOND * // 백만분의 일초
...

이러한 필드가 여러개가 있다. 사용할 때 주의해야할 점은

  • LocalDate는 날짜인데 사용하기 초를 나타내는 필드를 사용할거나 이러면 사용할 수 없다.
  • *이 표시되어 있는 필드는 int타입을 넘어가는 필드 이므로 getLong()을 사용해야한다.

필드의 값 변경하는법

필드의 값을 변경하기 위한 메서드로는 with(), plus(), minus()들이 존재한다.

날짜와 시간에서 특정 필드 값을 변경하려면, 다음과 같이 with로 시작하는 메서드를 사용하면 된다.

이러한 메서드들은 보통 새로운 객체를 생성해서 반환므로 대입연산자를 사용해야한다.

date = date.withYear(2000); // 년도를 2000년으로 변경한다.
time = time.withHour(12); // 시간을 12시로 변경

truncatedTo()메서드

이 메서드는 LocalTime에만 존재하며 LocalDate()는 존재하지 않는다.

왜냐하면 truncatedTo()는 짖ㅇ된 것보다 작은 단위의 필드를 모두 0으로 만드는 것이다.

LocalTime time = LocalTime.of(18, 12, 19); // 18시 12분 19초
time = time.turncatedTo(ChronoUnit.HOURS); // 18시 00분 00초가 된다.

이러한 기능이기 때문에 LocalDate()에서 만약 년도를 기준으로한다면 월,일이 0이되는데

0인 월, 일은 없기 때문이다.

날짜와 시간의 비교

LocalDateLocaltime의 날짜, 시간을 비교할 때는 compareTo()를 이용하면 되지만 그것보다 편리하게 비교할 수 있는 메서드 들이 존재한다.

boolean isAfter (ChronoLocalDate other); 
boolean isBefore (ChronoLocalDate other);
boolean isEqual (ChronoLocalDate other);

이중 isEqual메서드는 LocalDate에만 존재한다.

또한, isEqual은 오직 날짜만 비교한다. 한마디로 2018년 12월 19일이나 2019년 12월 19일은 같다는 것이다.

equals()는 모든 필드가 일치해야한다. 그러므로 2018년 12월 19일은 2019년 12월 19일은 다르다.

Instant

instant는 에포크 타임으로부터 경과된 시간을 나노초 단위로 표현한다.

이는 우리가 보기는 어렵다. (ex 1970-01-01 00:00:00 UTC)

하지만 단일 진법으로만 다루기 때문에 계산이 편하다.

생성하는 방법은 두가지 방법이있다. now()를 사용하는 것과 ofEpochSecond()를 사용하는 것과 같다.

Instant now = Instant.now(); 
Instant now = Instant.ofEpochSecond(now.getEposchSeocnd());
Instant now = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());

이러한 InstantDate로 변환되거 반대의경우고 다음과 같은 메서들ㄹ 통해 변환할 수 있다.

from(Instant instant); // Instant -> Date
toInstant() // Date -> Instant

LocalDateTime과 ZonedDateTime

LocalDateTimeZonedDateTime은 무언가 둘다 합쳐놓은 것이다.

  • LocalDateTime : LocalDate + LocalTime
  • ZonedDateTime : LoclaDateTime + 시간대

이렇게 구성이 되어있지만 이를 operand들을 합쳐서 결과를 생성할 있다.

물론 그냥 바로 생성해도 되지만 예를들자면 LocalDateTime으로 바꾸려고하는데 LocalDate는 현재 생성이 되있다면

LocalDate date = LocalDate.of(2018,12,19);

LocalDateTime dateTime = date.atTime(18,12,19); 

이런 방식으로도 생성할 수 있고 물론 반대도 존재하고 혹은 아무것도 없을 때, 아니면 둘다 있을 때 도 생성이 가능하다 여러가지 방법이 있으니까 참고하자

LocalDate date = LocalDate.of(2018, 12, 19);
LocalTime time = LocalTime.of(99, 03, 16);

LocalDateTime dt = LocalDateTime.of(date, time);
LocalDateTime dt1 = date.atTime(time);
LocalDateTime dt2 = time.atDate(date);
LocalDateTime dt3 = date.atTime(12, 8, 14);
LocalDateTime dt4 = time.atDate(LocalDate.of(2018, 8, 14));
LocalDateTime dt5 = date.atStartOfDay();

반대로 LocalDateTimeLocalDate또는 LocalTime으로 변환할 수 있다.

LocalDateTime dt = LoclaDateTime.of(2018,12,19,99,3,16);
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();

이를 LocalDateTime으로 ZonedDateTime으로 만들 수 있다.

왜냐하면 LocalDateTime + 시간대가 ZonedDateTime이기 때문이다.

LocalDateTime dt = LoclaDateTime.of(2018,12,19,12,3,16);

ZoneId zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = dt.atZone(zid); // 2018-12-19T12:03:16+09:00[Asia/Seoul]

이런식으로 atZone을 사용하여 시간대 정보를 넣어서 만들 수 있다.

이러한 ZonedDateTime을 이용하여 특정 시간대의 시간을 알기 위해서는 다음과 같이 하면된다.

ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);

TemporalAdjusters

이를 사용하면 특정 날짜 계산을 사용할 수 있다.

plus() , minus()를 사용해서도 계산 할 수 있지만 특정년도의 3주차 월요일 이런것들을 물어보면 계산하기 복잡하다 이러한 TemporalAdjusters클래스를 용하여 편하게 계산 할 수 있다.

LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));

위와 같은 방식들을 사용해서 계산할 수 있다. 사용할 수 있는 것들은 다음것들 등이 있다.

firstDayOfNextYear() // 다음해의 첫 날
lastDayForYear() // 올 해의 마지막 날

firstInMonth (DayofWeek dayOfWeek) // 이번 달의 첫 번째 ? 요일
next (DayofWeek dayOfWeek) // 다음 ?요일(당일 미포함)

...

이러한 다양한 메소드들이 존재한다.

TemporalAdjusters직접 구현하기

이러한 TemporalAdjusters를 직접 구현할 수도 있다.

TemporalAdjuster인터페이스를 사용하면 쉽게 구현할 수 있다.

이 클래스는 다음과 같은 추상 메서드 하나만 정의되어 있고, 이 메서드만 구현하면 된다.

@FuntionalInterface
public interface TemporalAdjuster{
    Temporal adjustInto(Temporal temporal);
}

이를 오버라이딩해서 자유롭게 구현하면 된다.

Period와 Duration

Period는 날짜와의 차이를 나타내고

Duration은 시간의 차이를 계산한다.

어떤 특정의 두 날짜와 시간을 구별하기 위해서는 between()이라는 메소드를 사용하면 편하다.

LocalDate date1 = LocalDate.of(2018,12,19);
LocalDate date2 = LocalDate.now();

Period pe = Period.between(date1, date2);

DurationPeriod에서 특정 필드의 값을 얻기위해서는 LocalDateLocalTime과 마찬가지로 get()을 사용한다.

하지만 이러한 값들을 Unit이나 Field를 가져올 때는 불안하거나 한정되어있다

그렇기 때문에 이를 안전하게 가져오기 위해서는 LocalTime이나 LocalDate로 변환해서 사용하는 것이 좋다.

이러한 PeriodDurationLocalTimeLocalDate처럼

of()with()메서드를 사용할 수 있고 또한, plus()처럼 사칙연산이나 isAfter()처럼 비교연산을 하거나 다른단위로 변환할 수 있는 메서드들이 존재하니 참고하면 좋을 것 같다.

파싱과 포맷

여기서의 파싱은 날짜와 시간을 원하는 형식으로 출력하고 해석하는 방법이다.

내가 원하는 타입으로 format()을 사용하여 형식을 정해줄 수도 있다.

이러한 종류는 다양하니 java api를 찾아보는 것도 좋을 것같다

문자열을 날짜와 시간으로 파싱하기

문자열을 날짜 또는 시간으로 변환하기 위해서는 static메서드인 parse()를 사용하면 가능하다.

이는 오버로딩된 메소드가 여러개가 존재한다. 예제를 보자

Local date = LocalDate.parse("2016-01-02", DateTimeFormatter.ISO_LOCAL_DATE);

DateTimeFormatter에 저장된 형식을 사용하여 정의된 형식으로 출력할 수도 있다.

또한, ofPattern()을 사용하여 파싱을 할 수도 있다.

ofPattern을 사용하면 원하는 형식에 따라 날짜 시간 분을 읽어올 수 있다.

DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

Refernece

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 쓰레드 (1)  (0) 2021.09.24
[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11

프로그램 오류의 종류

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우를

프로그램 에러 또는 오류라고 한다.

이를 종류로는 3가지 종류로 나눌 수 있다.

  • 컴파일 에러 : 컴파일 시에 발생하는 에러
  • 런타임 에러 : 실행 시에 발생하는 에러
  • 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

자바에서는 이러한 에러중 런타임 에러에 대해서 다음과 같이 2가지로 구분하였다.

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
  • 예외 : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류

에러는 프로그램 실행 도중 발생하면 복구 할 수 없는 심각한 오류들을 말한다.

예를들어 우리가 StackOverFlow같은 것이나 메모리누수 이러한 것들을 말한다.

예외는 프로그램에 의해 수습을 할 수 있다.

예외 클래스의 계층구조

이러한 에러와 오류는 Throwable이라는 클래스에 상속되어 있다.

Throwable은 결과적으로 클래스이기 때문에 Object에 상속이 되어있다.

ExceptionThrowableObject

ErrorThrowableObject

와 같은 계층 구조로 이루어져 있다.

이중 예외의 최고조상은 Exception클래스 이며 이러한 예외 클래스는 두 그룹으로 나누어진다.

  1. RuntimeException클래스와 그 자식들
  2. RuntimeException을 제외한 Exception클래스와 그 자식들

첫번 째 그룹은 보통 프로그래머의 실수로 발생하는 예외이다. 예를들어 NullpointerException같은 오류들이 있다.

두번 째 그룹은 보통 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외이다.

예를들어 프로그램을 사용하고있는 사용자가 입력 데이터형식을 잘못해서 들어가서 DataFormatException을 발생하거나 하는 것이다.

예외 처리하는 법

이러한 에러가 아닌 예외는 프로그래머가 미리 이에 대한 처리를 해줄 수 있다.

이러한 예외가 일어나면 프로그램이 종료되는데 이러한 것을 방직하고 정상적인 실행상태를 유지할 수 있게 하는 것이다.

이러한 예외를 처리하기 위해서는 try-catch문을 다음과 같이 사용한다.

try{
    //예외가 발생할 가능성이 있는 문장을 삽입
} catch (Exception1 e1){
        //Exception1 이 발생하면 이를 처리할 문장을 적는다.
} catch (Exception2 e2){

}

이러한 catch블럭은 여러개가 붙을 수 있지만 발생한 종류와 일치하는 단 한개의 catch블럭만 실행이 된다.

또한, try-catch문은 {}를 생략할 수 없다는 것을 알아두자

try{
    try{

    }catch(Exception e1){

    }
}catch (Exception e){

}

그리고 이렇게 try-catch문 안에 다시 반복해서 try-catch문을 넣는 행위도 금지한다.

또한, Exception의 참조변수는 중복될 수 없다. 그렇기 때문에 e를 썼다면 다른 예외의 참조변수는 다른 단어를 써야 할 것 이다.

try-catch문 예제

public static void main(String[] args) {

        int number = 100;
        int result = 0;
        System.out.println(number / result);

}

이러한 경우 다음과 같은 에러가 발생하여 프로그램이 강제 종료된다.

Exception in thread "main" java.lang.ArithmeticException: / by zero

이러한 ArithmeticException을 해결하기 위해서 try-catch문을 써보자

public static void main(String[] args) {
        int number = 100;
        int result = 0;
        try{
            System.out.println(number / result);
        }catch (ArithmeticException e){
            System.out.println(e);
            System.out.println("오류가 발생하여 0으로 출력됩니다.");
        }
}

이렇게 try-catch문으로 ArithmeticException을 예외처리해주면 catch 블럭 안에 있는 코드가 실행이 된다.

만약에 오류가 없다면 catch문장을 굳이 수행하지는 않는다.

그렇다고 오류가 있어도 무조건 들어가는 것이 아니라 **catch에 선언되어 있는 오류만 예외처리 할 수 있다.**

또한, 참조변수 e는 오류 내용을 그대로 가지고 있다. 그렇기 때문에 출력하면 다음과 같은 결과를 가진다.

java.lang.ArithmeticException: / by zero

이렇게 System.out.println이 있지만 이를 출력하는 다른 방법도 있다.

이중 오류 메세지인 / by zero만 출력하고 싶으면 getMessage()메서드를 사용하면 되고

모든 것을 다 출력하고 싶다면 printStackTrace()를 출력하면 된다.

catch (ArithmeticException e){
    e.printStackTrace();// java.lang.ArithmeticException: / by zero at codewords02.main(codewords02.java:12)
    System.out.prinln(e.getMessage()); // / by zero
}

이렇게 출력이 된다.

멀티 catch블럭

예외처리마다 똑같은 행위를 하고싶다면 예를들어 단순히 출력하는것이다.

이러한 경우 예외의 수가 많아지면 catch블럭 줄이 엄청 길어지게 되기 마련이다.

이를 해결하기위해서 JDK1.7부터 |를 사용해 예외들을 묶을 수 있게 되었다.

try{
    //...
}catch(Exception1 | Exception2 e){
    /..
    e.printStackTrace();
}

이런식으로 하나의 참조변수에 예외들을 묶을 수 있다.

하지만 이것도 규칙이 있다.

만약 Exception1Exception2가 서로 부모와 자식 관계에 있다면 컴파일 에러가 발생한다

왜냐면 부모와 자식관계에 있다면 부모 클래스만 적어도 자식클래스의 예외처리를 해줄 수 있기 때문이다.

사실 허용할 수 있는 정도지만 불필요한 코드를 제거하라는 의미에서 에러가 발생한다.

또한, 주의해야할 점이 참조변수가 같아진것 이기 때문에

어떤 오류가 발생할지 모르는 시점이기 때문에 자신의 멤버를 사용할 수 없고 부모의 멤버만 사용한다.

물론 장점도 있지만 이러한 점이 있기 때문에 겹치는 멤버나 메서드에 한에서 사용하기를 권장한다.

예외를 발생시키는 법

프로그래머는 다음과 같은 과정으로 예외를 고의로 발생시킬수 있다.

  1. 연산자 new를 이용하여 Exception객체를 생성하는 것이다
    • Exception e = new Exception("오류 내용");
  2. 키워드 throw를 사용하여 예외를 발생시킨다.
    • throw e;
    • throw new Exception();

하지만 이러한 예외중에서도 RuntimeException을 고의로 발생시킬 때는 컴파일이 잘되는 것을 볼 수 있다.

왜냐하면 RuntimeException과 그 자식 클래스들을 프로그래머에 의해 실수로 발생하는 것들이기 떄문에 예외처리를 강제하지 않는다는 것이다.

이들은 unchecked예외라고 불린다. 나머지들은 checked예외라고 불리낟.

메서드에 예외 선언하는 법

메서드에 예외를 선언하기 위해서는 throws를 사용하여 처리할 수 있다.

void method() throws Exception1, ....[

}

다 귀찮다 하면 예외의 최고 조상인 Exception클래스를 메서드에 선언하면 모든종류의 예외가 발생할 가능성이 있다는 것을 표시한다.

이렇게 예외를 선언한다면 자식 클래스가 상속하여 오버라이딩할 때도 예외가 발생할 수 있다는 점에 주의해야한다.

그렇기 때문에 자바는 메서드를 작성할 때 메서드 내에서 발생할 가능성이 있는 예외를 메서드의 선언부에 명시하여 이 메서들ㄹ 사용하는 쪽에서는 이에 대한 처리를 하도록 강요하기 때문에 좀더 견고한 프로그램 코드를 작성하게 도와준다.

그렇기 때문에 메서드를 사용할 때 API를 보고 throws목록을 확인하고 예외를 처리해줘야할 생각을 하는 것이 좋다.

예외의 호출 스택(Call Stack)

예외는 처리를 하지 않으면 호출한 메서드에게 넘겨주게된다.

public static void main(String[] args) throws Exception
{
    method();
} 

public static void method() throws Exception{
    throw new Exception();
}

이러면 다음과 같은 오류 메세지가 등장하게된다.

image

오류가 등장하는것을 보면 mehtod()에서 먼저 오류가 발생한뒤 처리를 해주지 않아 method를 호출한 main에게 넘겨주고 main에서도 처리를 안해주어 결국 예외가 발생한것으로 보인다.

이를 해결하기위해서는 두가지 방법이있다.

try-catch를 이용하여

  1. main메서드에서 처리해주는것
  2. mehtod()에서 처리를 해주는것

둘중 어느 것을 사용해도 무방하다

하지만 첫번째 방법은 보통 메서드 내에서 자체적으로 해결이 안되는 경우에 사용한다. 예를들어 호출값을 다시 받아서 처리해주는 경우가 있다.

그게 아니라면 보통은 예외가 발생한 해당 메서드에서 처리해주는 것이 좋다.

Finally블럭

Finally블럭은 예외 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.

try-catch문의 끝에 선택적으로 덧붙여 사용하면된다.

try{

}catch(Exception e1){

}finally{

}

중요한것은 예외가 발생하지 않아도 실행되고 혹은 예외가 발생해도 실행된다는 것이다.

try-with-recsoorces문

JDK 1.7부터 새로 추가된 기능으로 보통 파일 입출력에서 사용한다.

왜 입출력에서 자주쓰이는지는 다음 예를 살펴보자

try{
    fileInputStream = new FileInpuStrem("score.bat");
    dataInputStream = new DataInputStream(fileInputStream);
    //...
} catch(IOException e){
    e.printStackTrace();
} finally{
    dataInputStream.close();
}

이러한 파일 입출력은 사용한 후에 꼭 close()를 해주어야 사용된 자원이 반환된다.

이러한 특성을 지니고 있기 때문에 만약 예외가 발생하면 close가 되지않아 자원이 반환되지 않기 때문에 finally에서 항상 close()를 해주고 있다.

하지만 이 코드에는 문제점이 있다. close()라는 메서드에서도 오류가 발생할 수 있다는 것이다.

프로그래머가 잘못해서 다른 곳에서 close()이미 했다던지 혹은, 파일이 없어서 close()할 파일도 없다던지 하는 오류가 발생할 수 있다. 그래서 다음과 같이 작성해야한다.

 try{
    fileInputStream = new FileInpuStrem("score.bat");
    dataInputStream = new DataInputStream(fileInputStream);
    //...
} catch(IOException e){
    e.printStackTrace();
} finally{
    try{
        if(dataInputStream != null)
            dataInputStream.close();
    }catch (IOException e){
            e.printStackTrace();
    }
}

이런식으로 finally블럭에 try-catch문을 추가하여 close()에서 발생할 수 있는 예외를 처리하도록 변경하였다.

이러한 문장을 try-with-resources문으로 고친다면 다음과 비슷하게 사용될 수 있다.

try (FileInpuStream fileInputStream = new FileInputStream("score.bat");
        DataInputStream dataInputStream = new DataInputStream(fileInputStream)){
    while(true){
        score = dataInputStream.readInt();
        System.out.prinln(score);
        sum += score;
    } catch (EOFException e) {
        System.out.println("점수의 총합은 " sum + "입니다.");
    } catch (IOException ie){
        ie.printStackTrace();
    }
}

이렇게 변경하면 앞선 코드보다 복잡하지 않고 좋다.

현재 try의 괄호 안에 객체를 생성하는 문장을 넣었는데 이는 try를 벗어나자마자 자동적으로 close()를 호출하게 된다.

하지만 사용하기 위해서는 객체의 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

사용자 정의 예외 만들기

기존 정의된 예외말고 자신이 필요한 예외클래스를 새롭게 정의 할 수 있다.

Exception클래스나 RuntimeException클래스로부터 보통은 상속받아 클래스를 작성한다.

// 예시 1
class MyException extends Exception{
    private final int ERR_CODE;

    MyException(String message){
        super(message, 100);
    }

    MyException(String message, int errCode){
        super(message);
        ERR_CODE = errCODE;
    }

    public int getErrCode(){
            return ERR_CODE;
    }        
}

클래스를 만약 Exception을 하면 checked예외로 처리가 된다.

만약 RuntimeException을 상속 받았다면 nonchecked예외로 처리할 수 있어서 더 환영받고 있다.

예외 되던지기

만약, 한 메서드에서 발생할 수 있는 예외가 여럿인 경우는 다음과 같이 처리할 수 있다.

  • 몇 개는 try-catch문을 이용하여 메서드 내에서 자체로 처리한다.
  • 나머지는 선언부에 지정하여 호출한 메서드에서 처리한다.

이런식으로 양쪽을 통해 처리할 수 있다. 또한, 이런 방법으로도 예외가 단 하나인 상황에서도 처리할 수 있다.

이렇게 예외가 단 하나인 경우에는 예외를 처리한 후에 다시 발생시키는 방법을 통해서 가능하다.

이를 예외 되던지기라고 한다.

예외 되던지기를 사용하는 방법은 다음과 같다.

  • 예외가 발생할 가능성이 있는 메서드에서 try-catch문을 사용하여 예외를 처리해준다.
  • catch문에서 필요한 작업을 행한 후 throw문을 이용하여 예외를 다시 발생시킨다.
  • 다시 발생한 예외를 이 메서드를 호출한 메서드에게 전달되고 호출한 메서드의 try-catch문에서 예외를 또 다시 처리한다.

예외 되던지기를 사용할 때는 하나의 예외에 대해서 예외가 발생한 메서드와 이를 호출한 메서드 양쪽 모두에서 처리해줘야할 작업이 있을 때 사용한다.

// 예제
public static void main(Stirng[] args){

    try{
        method();
    } catch (Exception e){
        System.out.println("main 메서드에서 예외가 처리되었습니다");
    }
} 

static void method() throws Exception{
    try{
        throw new Exception();
    } catch (Exception e){
        System.out.println("method 에서 예외가 처리되었습니다.");
        throw e;
    }
}

이런식으로 main에서도 try-catch를 이용하여 처리를 해주고 method에서 예외를 처리하고 예외를 방생시켜서 사용한다.

연결된 예외(chained exception)

한 예외는 다른 예외를 발생시킬 수 있다.

다음 예시는 SpaceException을 원인 예외로하는 InstallException을 발생시키는 예시이다.

public class ChainedException {
    public static void main(String[] args) {
        try{
            install();
        } catch (InstallException e){
            e.printStackTrace();
        }
    }

    static void install() throws InstallException{
        try{
            startInstall();
            copyFile();
        } catch (SpaceException se){
            InstallException ie = new InstallException("설치중 예외 발생");
            ie.initCause(se);
            throw ie;
        } catch (MemoryException me){
            InstallException ie = new InstallException("설치중 예외 발생");
            ie.initCause(me);
            throw ie;
        } finally {
            deleteTempFiles();
        }
    }

    static void startInstall(RuntimeException ) throws SpaceException, MemoryException{
        if(!enoughSpace()){
            throw new SpaceException("설치할 공간이 부족합니다.");
        }

        if(!enoughMemory()){
            throw new MemoryException("메모리가 부족합니다.");
            throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
        }
    }

    static void copyFile(){

    }

    static void deleteTempFiles(){

    }

    static boolean enoughSpace(){

        return false;
    }

    static boolean enoughMemory(){

        return true;
    }
}

class InstallException extends Exception{
    InstallException(String message){
        super(message);
    }
}

class SpaceException extends Exception{
    SpaceException(String message){
        super(message);
    }
}

class MemoryException extends Exception{
    MemoryException(String message){
        super(message);
    }
}

일단 initCase()는 지정한 예외를 원인 예외로 지정하는 메서드이다. 또한, getCause()는 원인 예외를 반환하는 메서드이다.

원인 예외로 왜 등록해서 다시 예외를 발생하는지는 이유가 있다.

그냥 예외만 처리하면되는데 왜이렇게 복잡하게 하는지 궁금할텐데

그냥 예외만 바로 처리해버린다면 실제로 발생한 예외가 무엇인지 모르기 때문에 함부로 할 수 없다.

또한, checked예외라면 이를 unchecked에외로 바꿀 수 있기 떄문이다.

startInstall()메서드를 확인해보면 자세히 알 수 있다.

static void startInstall(RuntimeException ) throws SpaceException, MemoryException{
        if(!enoughSpace()){
            throw new SpaceException("설치할 공간이 부족합니다.");
        }

        if(!enoughMemory()){
            throw new MemoryException("메모리가 부족합니다.");
            throw new RuntimeException(new MemoryException("메모리가 부족합니다."));
        }
    }

이처럼 throw new RuntimeException으로 한번 감싸준다면 unchecked예외로 설정할 수 있다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 지네릭 타입  (0) 2021.09.19
[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11

내부 클래스란?

내부 클래스는 말 그대로 안에 선언된 클래스이다.

클래스 내에 선언된 클래스를 내부 클래스라고 한다.

내부 클래스를 사용하는 이유는 다음과 같다.

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  • 코드의 복잡성을 줄일 수 있다. (캡슐화)

이 때 사용하는 내부 클래스는 다른 클래스에서는 사용하지 않는 내용이여야 한다.

내부 클래스의 종류

  • 인스턴스 클래스
    • 외부 클래스의 메멉변수 선언위치에 선언한다.
    • 외부 클래스의 인스턴스멤버 처럼 다루어진다.
    • 주로 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다.
  • 스태틱 클래스
    • 외부 클래스의 멤버변수 선언위치에 선언한다.
    • 외부 클래스의 static멤버 처럼 다루어진다.
    • 특히 static메서드에서 사용될 목적으로 선언된다.
  • 지역 클래스
    • 외부 클래스의 메서드나 초기화블럭 안에 선언한다.
    • 선언된 영역 내부에서만 사용될 수 있다.
  • 익명 클래스
    • 클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스이다. (일회용)

이러한 내부 클래스의 선언위치는 다음과 같다.

class InnnerClassTest{
    class 인스턴스클래스 {}
    static class 스태틱 클래스 {}

    void method(){
        class 내부클래스{}
    }

}

내부 클래스의 제어자와 접근성

내부클래스는 앞에 선언위치와 같이 그냥 Class 내부에 선언되는 것이다.

이러한 내부 클래스도 클래스이기 때문에 abstractfinal같은 제어자를 안에서 사용할 수 있다. 또한, 접근 제어자도 마음대로 사용할 수 있다.

내부 클래스에서 static을 사용하기 위해서는 static클래스로 정의를 해야한다.

그렇지 않으면 static을 사용해야하는데 클래스가 안읽혀서 static을 사용하지 못하는 모순이 생긴다.

하지만 final static은 상수로서 사용이 가능하다.

또한, static멤버는 인스턴스 멤버를 직접호출을 할 수 없다.

하지만 인스턴스 멤버는 인스턴스 멤버와 static멤버를 모두 직접호출이 가능하다.

static클래스는 외부의 클래스의 인스턴스 멤버를 객체 생성없이 사용할 수 없지만

인스턴스 멤버는 클래스의 인스턴스멤버를 객체 생성 없이 사용할 수 있다.

지역 클래스는 외부 클래스의 인스턴스 멤버와 static 멤버를 모두 사용할 수 있다.

또한, 지역클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다.

final이 붙은 지역변수만 접근이 가능하다. 하지만 JDK1.8이후로는 final을 컴파일러가 자동적으로 붙여준다.

컴파일을 하면 외부 클래스명$내부 클래스명.class형식으로 된다. 하지만 안에 중복된 이름의 클래스가 있을 수도 있기 때문에 외부 클래스명$숫자 내부 클래스명.class로 이루어진다.

마지막으로 내부 클래스와 외부 클래스에 선언된 변수의 이름이 같을 떄가 있다.

이럴 때는 외부 클래스명.this를 붙여서 서로 구별 할 수 있다.

익명 클래스(anonoymous class)

익명 클래스는 내부 클래스와 다르게 일므이 없다.

클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직하나의 객체만 생성할 수 있는 일회용 클래스이다.

이는 다음과 같이 생성된다.

new 부모 클래스이름(){

}

new 구현 인터페이스이름(){

}

Object ob = new Object(){ void method() };

이는 이름이 없어서 생성자를 가질 수 없다.

또한, 구현하고자 하는 부모 클래스이름구현 인터페이스 이름을 사용하기 때문에

둘이상의 인터페이스를 구현할 수 없다.

하나의 클래스를 상속 받거나 단 하나의 인터페이스만을 구현할 수 있다.

익명 클래스를 사용하면 다음과 같은 형태로 클래스파일이 생성이된다.

외부 클래스명&숫자.class

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] java.time 패키지  (0) 2021.09.18
[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11
[Java/Study] 다형성  (0) 2021.09.10

인터페이스란?

인터페이스는 일종의 추상클래스이다.

인터페이스는 추상클래스보다 추상화 정도가 높다.

그래서 오직 추상메서드상수만을 멤버로 가질 수 있다.

추상클래스가 미완성 설계도이면 인터페이스는 그냥 설계도이다.

인터페이스의 작성법

인터페이스를 작성하기 위해서는 다음과 같이 작성한다.

interface 인터페이스이름{
        public static final 타입 상수이름 = 값;
        public abstract 메서드이름(매개변수);
}

인터페이스의 이러한 작성법을 본다면 특이한 사항이 있다.

인터페이스를 작성하기 위해서는 지켜야할 제약사항이 존재한다.

  • 모든 멤버변수는 public static final이어야 하며 이를 생략할 수는 있다.
  • 모든 메서드는 public abstract이어야 하며 이를 생략할 수 있다.
  • static메서드와 default메서드는 JDK1.8부터 예외로 한다.

만약 이러한 public static final이나 public abstract를 생략을 한다면 알아서 컴파일러가 컴파일시 자동으로 추가해 준다.

인터페이스의 특징들

인터페이스의 상속

인터페이스는 인터페이스 끼리만 상속이 가능하다. 또한, 클래스와 달리 다중상속을 허용한다.

또한, 인터페이스는 클래스와 달리 Object같은 최고 부모가 존재하지 않는다.

인터페이스의 구현

인터페이스를 클래스에 사용할 때 구현이라는 말이 쓰인다.

인터페이스는 추상클래스와 비슷하다고 하지 않았는가?

그렇기 때문에 추성클래스와 비슷하게 인스턴스를 그 자체로 생성이 불가능하다.

클래스 간의 상속은 extends확장이라는 의미로 사용하지만

인터페이스는 달리 이러한 설계도를 완성을 해야한다는 의미이기 때문에 구현이라는 의미로 implements를 사용하여 클래스를 사용한다.

class 클래스이름 implements 인터페이스 이름{

}

//예시
class Dog implements Animal{
        /...
}

인터페이스를 implements한다면 이 인터페이스에 있는 모든 메서드를 구현해야 한다.

하지만 일부 메서드만 구현하고 싶을때도 있다. 그럴 때는 abstract를 붙여서 추상클래스로 만들면 가능하다.

abstract class Dog implements Animal{
        /...
}

지금까지 인터페이스구현이라는 단어를 사용하고 클래스확장이라는 단어를 사용하기 때문에

단어의 의미가 달라서 인터페이스를 사용하면 부모가 없다고 생각할 수도 있지만 그것은 아니다.

인터페이스로 부터 상속받은 추상메서드를 구현하는 것이기 때문에 지금까지 사용했던 부모 라는 의미와는 100% 같지는 않지만 조금 다른의미의 조상이라고 볼 수 있다.

접근제어자의 사용

다음과 같은 class와 인터페이스가 있다.

interface Animal{
    void feed(); // public abstract void feed();
}

class Dog implements Animal{
        public void feed(){

        }
}

이때 Dog클래스에서 feed()메서드를 사용하기 위해서는 반드시 public으로 선언을 해야 사용할 수 있다.

왜냐하면 오버라이딩 할때는 부모의 메서드보다 넓은 범위의 접근 제어자를 지정해야하기 때문에 public abstract보다 넓은 범위를 사용하기 위해서 public을 반드시 사용해야 한다.

인터페이스를 사용한 다중상속

자바에서는 다중 상속을 금지하는 이유는 만약 두 부모로 부터 상속받는 멤버변수중 겹치는 것이있다면 컴파일러는 어떤 조상의 것을 상속받아서 사용했는지 알수가 없기 때문에 이를 금지하였다.

하지만 인터페이스는 다중 상속이 된다는것이 이상하지 않는가?

왜냐하면 인터페이스static상수만 정의할 수 있기 떄문에 부모클래스의 멤버변수와 충돌하는 겨우는 거의 없다.

그리고 static상수는 보통 클래스 이름을 붙여서 사용하기 때문에 충돌되더라도 괜찮다.

만약, 메서드가 같아서 걱정이라면 당연히 부모 클래스의 메서드를 상속받으면되기 때문에 문제가 없다.

이렇게 인터페이스는 부모클래스와 충돌하지 않기 때문에 이를 이용하여 단일상속만 가능한 자바에서 다중상속을 구현할 수 있다.

다음과 같은 예제를 살펴보자

public class Apple{
    protected int sweet;
    protected int growLevel;    

    public void feed(){
        growLevel++;
    }

public class Dessert{
        protected int price;

        public void eat(){
            //.. 먹는것을 구현
        }
        public int getPrice(){
            return price;
        }
        public void setPrice(int price){
            this.price = price;
        }
    }
}

이러한 Apple이라는 클래스와 Dessert라는 클래스를 다중상속하여 AppleDessert라는 클래스를 만들고 싶을 떄 단일상속을 원칙으로 하는 자바에서는 구현을 못할 것이라고 생각하지만

인터페이스를 사용하여 구현할 수 있다. 다음과 같은 인터페이스를 구현한다.

public interface InterfaceAppleDessert{
        public abstract void eat(); 
        public abstract int getPrice();
        public abstract void setPrice(int price);
} 

InterfaceAppleDessert라는 인터페이스는 Dessert클래스에 정의된 메서드와 일치하는 추상메서드를 가지는 인터페이스다 이를 이용하여 실제로 만들 AppleDessert라는 클래스를 구현해보자

public class AppleDessert extends Apple implements InterfaceAppleDessert{
        Dessert des = new Dessert();
        public void eat(){
            des.eat();
        }        
        public int getPrice(){
            return des.getPrice();
        }
        public void setPrice(int price){
            desc.setPrice(price);
        }
}

이렇게 InterfaceAppleDessert인터페이스 때문에 메서드를 반드시 오버라이딩 할 수 밖에 없다.

이러한 메서드를 Dessert인스턴스를 만들어 Dessert 메서드를 각각의 오버라이딩된 메서드에 호출하면 다중상속을 구현할 수 있다.

인터페이스를 이용한 다형성

다형성에서는 자식클래스의 인스턴스를 부모타입의 참조변수로 참조가 가능하다는 것을 배웠다.

인터페이스 또한, 이를 구현한 클래스의 부모라고도 볼 수 있기 때문에 이를 형변환하거나

인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있다.

이전 예시를 찾아보자 Dog클래스와 Animal인터페이스를 살펴보면 다음과 같다.

// 예시 1번
Animal animal = (Animal)new Dog();

//예시 2번
Aniaml animal = new Dog();

또한, 클래스의 다형성과 같이 매개변수와 리턴타입에도 사용이 가능하다.

void bark(Animal animal){
    //...
}

Animal method(){
        Dog dog = new Dog();
        return dog;
}

리턴타입이 인터페이스이면 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

분명 method()의 반환타입은 Animal이라는 인터페이스인데 implementsDog클래스의 인스턴스가 반환되는 것을 볼 수 있다.

인터페이스의 장점

  • 개발시간을 단축시킬 수 있다.
    • 인터페이스가 작성되면 여러명이서 각각 인터페이스를 구현할 수 있다. 예를들어서 Animal이라는 인터페이스를 개발해놓으면 2명이서 Dog클래스를 만들필요없이 분담하여 Cat클래스도 만들 수 있는 것이다.
  • 표준화가 가능하다
    • 프로젝트의 기본틀을 인터페이스로 작성하여 다른 개발자들이 개발할 떄 또한, 일관되고 정형화된 프로그램의 개발이 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어 줄 수있다.
    • 아무런 관계를 가지지 않고 부모클래스도 다른 클래스에게 여러개를 implements가 가능한 인터페이스를 사용하여 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어 줄 수 있다.
  • 독립적인 프로그래밍이 가능하다
    • 인터페이슨느 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제구현에 독립적인 프로그램을 작성하는 것이 가능하다. 클래스와 클래스의 직접적인 관계를 인터페이스를 이용하여 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 미치지 않는 독립적인 프로그래밍이 가능하다.

인터페이스의 이해

인터페이스를 이해하기 위해서는 다음과 같은 두가지 사항을 염두해둬야한다.

  • 클래스를 사용하는 쪽과 클래스를 제공하는 쪽이 있다.
  • 메서드를 사용하는 쪽에서는 사용하려는 메서드의 선언부만 알면 된다.

다음과 같은 예시를 살펴보자

class A {
    public void methodA(B b){
        b.methodB();
    }
}

class B{
    public void methodB(){
        System.out.println("this is method B");
    }
}

public static void main(String[] args) {
        A a = new A();
        a.methodA(new B());
}

현재상황은 A클래스가 B의 인스턴스를 생성하고 메서드를 호출한다.

이때 A를 사용하는 쪽이라하고 B를 제공하는 쪽이라하자.

이렇게 직접적으로 연결되어 있다면 만약 B라는 클래스가 변경이된다면 예를들어 methodB()가 사라지거나 혹은 B라는 클래스가 변경이되면 A라는 클래서의 메서드도 변경을 반드시 해야한다.

이렇게 직접적으로 관계되어있는 둘의 관계를 바꾸기 위해서는 인터페이스를 사용하여 클래스의 선언과 구현을 분리해야한다. 다음과 같은 인터페이스를 생성해보자

interface I {
    public abstract void methodB();
}

class B implements I{
    System.out.println("this is method B");
}

class A{
        public void methodA(I i){
            i.methodB();
        }
}

이런식으로 인터페이스 I를 만들어서 B의 클래스를 implements하였다.

선언은 인터페이스가 맡아서 하였기 때문에 내가 만약에 이제 클래스 B를 쓰지 않는다고 해도

C라는 클래스를 생성하여 대체한다고 하여도 implementsC클래스에 한다면 문제가 없어진다.

혹은 다음과 같은 예제를 보자

interface PayInterface{
    public abstract void paymentMethod(); 
}

class Card implements PayMethod{
    public void paymentMethod(){
        System.out.println("카드로 결제하겠습니다.");
    }
}

class Cash implements PayMethod{
    public void paymentMethod(){
        System.out.println("현금으로 결제하겠습니다.");
    }
}

class PayService(){
    public void buy(PayInterface i){
            i.paymentMethod();
    }
}

public static void main(Stirng[] args){
    PayService service = new PayService();

    service.buy(new Card()); //카드로 결제하겠습니다.
     service.buy(new Cash()); //현금으로 결제하겠습니다.
}

인터페이스를 사용하여 내가 원하는 방식대로 인터페이스만 implements한다면 사이트에서 결제방식이 바뀌더라도 직접적으로 PayService와 결제방식 클래스를 둘다 안바꾸어도 되고 그냥 PayInterfaceimplements한 하나의 클래스만 바로 생성하면 바로 쓸수있다.

디폴트 메서드와 static메서드

이부분은 앞선 설명으로 알듯이 JDK1.8버전 이후에 생긴 것이다.

원래는 인터페이스는 추상 메서드만 선언이 가능하다.

java.util.Collection 인터페이스는 static메서드를 선언을 할 수 없었어서 Collections라는 킆래스에 들어가게 되었다.

static메서드는 많이 들어봤지만 디폴트 메서드는 처음일 것이다.

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드이 이다.

추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가된 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다. 굳이 메서드를 오버라이딩 할 필요가 없기 때문이다.

// 선언방법
default void newMethod() {}

앞에 default를 붙이면된다.

디폴트 메서드는 추상 메서드와 달리 ;만 붙여서 끝나는 것이 아닌 {}부분이 필요하다.

하지만 이러한 디폴트 메서드도 구현하다 보면 만약에 기존 메서드와 이름이 충돌되는 경우가 있을 것이다. 이럴 때는 다음과 같은 규칙을 지켜야한다.

  1. 여러 인터페이스의 디폴트 메서드 간의 충돌
    • 인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해야 한다.
  2. 디폴트 메서드와 부모 클래스의 메서드 간의 충돌
    • 부모 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다.

그냥 편하게 하려면 디폴트 메서드라도 오버라이딩을 하면 그만이다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 예외처리  (0) 2021.09.14
[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 추상클래스  (0) 2021.09.11
[Java/Study] 다형성  (0) 2021.09.10
[Java/Study] 제어자  (0) 2021.09.09

추상클래스란?

추상 클래스는 이전에도 설명했듯이 미완성 설계도에 비유할 수 있다.

미완성 메서드를 가지고 있기 때문에 이에 비유한다.

추상 클래스는 인스턴스를 생성할 수 없다.

상속을 통한 자식클래스에서 메서드의 내용을 구현하게 된다.

추상클래스를 사용하기 위해서는 다음과 같이 abstract키워드를 사용하면된다.

abstract class 클래스이름{

}

추상 메서드가 없더라도 앞에 abstract를 넣으면 추상 클래스로 사용이 가능하다.

추상메서드란?

추상 메서드는 선언만 해놓고 구현을 안해놓은 메서드라 할 수 있다.

실제 수행될 내용은 적어놓지 않았지만 이런 기능이 있을거다 라고 모호하게 작성한 것을 말한다.

왜 구현을 안하고 남겨 놓는 이유는 상속받는 클래스에 따라 메서드 내용이 달라질 수 있기 때문이다.

예를들어 Animal이라는 클래스에 추상클래스인 talk()가 있다면

이를 CatDog가 상속을 받았다면 서로 다른 내용으로 짖기 때문에 다를것 이다.

추상 클래스와 메서드는 다음과 같이 사용된다.

abstract class Animal{
    abstract void talk();
}

class Cat extends Animal{
    void talk(){
        System.out.println("미야옹");
    }
}

class Dog extends Animal{
        void talk(){
            System.out.println("멍멍");
        }
}

추상 메서드는 특이할 때 선언할때 abstract를 붙이고 마지막은 꼭 메서드와 다르게 ;를 붙여주는 것이 특징이다.

이러한 추상 클래스를 통해서 각 클래스에 공통된 메서드가 있다면 추상 클래스를 만들어 재사용하기 쉽게 만드는 것도 한가지 방법이다.

이렇게 공통된 부분으로 묶어서 추상 클래스를 만들어 상속시키면 다양한 장점이 생긴다.

  • 같은 부모를 가져 배열로 묶어서 관리할 수 있다.
  • 코드가 깔끔해진다.

Reference

  • 자바의 정석

'Java > Java' 카테고리의 다른 글

[Java/Study] 내부 클래스  (0) 2021.09.11
[Java/Study] 인터페이스  (0) 2021.09.11
[Java/Study] 다형성  (0) 2021.09.10
[Java/Study] 제어자  (0) 2021.09.09
[Java/Study] Package 와 Import 및 터미널로 자바 실행방법  (0) 2021.09.08

자바에서 문자열의 앞뒤에 공백이 들어가는 경우가있다.

이런경우에 없애는 쉬운 메서드가 있다. trim()이다 사용방법은 다음과 같다.

String hello = " Hello World ";
System.out.println(hello.trim()); // "Hello World" 로 출력됨

이런식으로 가운데에 있는 공백은 지우지 않고 양 끝에 있는 공백만 지우게 된다.

'Java > 짜잘한팁' 카테고리의 다른 글

[Java/Tip] 문자열자르기  (0) 2021.09.11

+ Recent posts