빅데이터 Java 개발자 교육 - 05일차 [예외(오류처리) , static 정적, 제너릭]

Jun_Gyu·2023년 2월 25일
0

예외(Exception)

오류처리

프로그래밍에서 예외는 에러라고 보면 된다. 예외가 발생하는 이유는 예기치 못한 상황의 발생, 개발자의 설계or코딩실수 등 여러 요인들 때문이라고 볼 수 있겠다.


이러한 예외의 종류에는 크게 2가지가 있다.
컴파일 시 발생하는 컴파일 에러와 프로그램이 구동되는 중에 발생하는 런타임 에러가 있다.

보통 컴파일 오류의 경우 문법상의 오류 때문에 발생하고, 런타임 오류의 경우 잘못된 설계로 인해 발생하는 경우가 많다.

(요리로 치자면 컴파일 오류는 재료의 문제, 런타임오류는 조리과정의 문제라 볼 수 있겠다.)

그리고 자바에서는 이러한 문제를 해결하기 위해

java.lang.Exception

이라는 클래스를 기본적으로 제공하고 있다.

자바에서 예외처리는 기본적으로 아래 코드로 이루어진다.

try {
	// 실행 될 코드
} catch (예외 클래스) {
	// 예외처리 코드
} finally {
	// 예외와는 상관없이 무조건 실행되는 코드
}

try 블록에 실제 실행되어야 하는 코드가 들어가며, 예외(Exception)가 발생할 가능성이 있는 코드가 들어간다.

catch 블록에는 Exception이 발생하면 실행되는 코드가 들어간다. 즉 예외처리를 하는 코드다.

그리고, finally 블록에는 어떠한 예외가 발생하더라도 반드시 실행되어야 하는 코드가 들어가는 곳이다.

그렇다면 이러한 기능은 어떤 경우에 사용을 할 수 있을까??

사용자가 개발자의 의도와 다른 행동을 하여 예외가 발생하는 경우, 개발자가 미리 해당 상황에 대해 예외처리를 해줄 수 있다.

(예를들어 생년월일을 적는 칸에 본인 이름을 쓴다던가..)

그렇게 하여 프로그램이 예기치 못한 상황에서 비정상적으로 종료되는 것을 막고, 정상 작동하게 할 수 있다.

이러한 예외적인 상황 발생에 대해 미리 대처하는 것을 예외처리라고 한다.

예를 한번 들어보자.


package review;

public class Review1 {
	public static void main(String[] args) {
		int a = 10;
		int b = 0;
		int c = a / b;
		System.out.println(c);
	}
}

위의 코드를 Ctrl + F11을 눌러 Run 시키면 10을 0으로 나눌 수 없기 때문에 아래처럼 오류가 발생하게 된다.

그래서 코드에 try catch 문을 추가해 다시 실행시켜보면

package review;

public class Review1 {
	public static void main(String[] args) {
		try {     // 실제 실행시키려는 코드
			int a = 10;
			int b = 0;
			int c = a / b;
			System.out.println(c);
		} 
		catch (Exception e) {     // 예외 발생시 출력되는 코드.
			System.err.println(e.getMessage());     // 에러 메세지 출력
			e.printStackTrace();     // 오류를 상세히 출력해줌. 개발자를 위해서!

		} 
		finally {     // 오류와 상관없이 무조건 출력되는 코드.
			System.out.println("계산 완료.");
		}
	}
}

아래와 같이 출력되게 된다.

위와 같은 방법의 경우에는 클래스 내에서 직접 Exception에 대해서 대비를 하는 방식이지만,

하청을 맡기는 기업의 입장에서 본인들의 니즈에 맞게끔 Exception에 대해 대비할 수 있도록

코드 수정이 용이하도록 메인 클래스 등으로 책임전가(?)를 할 수 있다.

예외 던지기 (throw, throws)

오류처리

일단 throw의 경우 개발자가 고의적으로 예외를 발생시키는 것이다.

'throw'라는 키워드를 이용하며, 앞서 이야기한 내용처럼 주로 비즈니스 로직을 구현하는 과정 중에서 사용하며, 컴파일에는 문제가 없지만 해당 비즈니스 로직이

개발자가 의도한 대로 통과하지 못했을 경우일때 주로 사용한다.

throws의 경우 메서드 내에서 예외처리를 하지 않고 해당 메서드를 사용한 곳에서 예외 처리를 하도록 예외를 위로 던지는 것이다.

말 그대로 Exception 자체를 전가하는 것이다.

예시를 들어보자.


package review;

public class Review1 {
	public static void main(String[] args) {
		int a = 2;
		int b = 1;
		System.out.println("정답은 : " + divide(a, b));
	}

	public static int divide(int a, int b) {
		if (b == 1) {
			throw new ArithmeticException("나눌 수 없습니다.");
		}
		return a / b;
	}
}

위의 코드와 같은 경우 2나누기 1을 출력하는데 있어 계산 자체에는 문제가 없지만,if문의 throw를 사용하여 b의 값이 1인 경우 강제로 나눌 수 없다고

throw를 사용해 예외를 발생시킨 코드이다.

이를 실행시키면

이와같이 Exception이 발생한 모습을 확인할 수 있다.
그리고 throws의 경우는 아래와 같다.

package review;

public class Review1 {
	public static void main(String[] args) {
		int a = 2;
		int b = 1;
		try {
		System.out.println(divide(a, b));	     // 실행하려는 코드
		}
		catch (ArithmeticException e) {     // 예외 발생시 실행되는 코드
			e.printStackTrace();
		}
	}

	public static int divide(int a, int b) {
		if (b == 1) {
			throw new ArithmeticException("나눌 수 없습니다.");
		}
		return a / b;
	}
}

divide 클래스에서 예외를 해당 메소드를 사용하는 클래스로 책임 전가하여 메인 클래스에서 직접 try catch문을 지정하게 된다.

결과는 위의 사진과 같이 출력된다.

throw를 사용하는 이유는 예외가 발생할 수 있는 코드가 있다는 것을 인지시키고 예외처리를 강요하여 여러 가지 발생 가능한 예외들을 호출한 메서드에서 한 번에 처리할 수 있게 하여 관리를 용이하게 할 수 있기 때문이다.

아래는 예외 출력문의 여러 코드들 중 하나이니 참고하자.

System.err.println(e.getMessage());   // 에러문 출력
e.printStackTrace();  // 오류메세지 출력

static 정적

객체 수를 줄여 자원을 아끼자

코딩을 하면서 고려해야 할 것은 코드를 짜고 설계를 하는데 있어 같은 연산을 수행하더라도 좀 더 간결하고 깔끔하게 코드를 짜는 것이다.

간결해질수록 클래스 내의 객체는 줄어들것이고, 객체가 많이 줄어들수록 메모리 자원을 아낄 수 있는 효과를 가져오게 된다.


이렇게 객체의 수를 줄여서 자원을 아낄 수 있는 방법이 있는데 바로 static 정적을 이용하는 방법으로, static변수와 static메소드 두가지를 생성하여 객체를 줄일 수 있다.

(Static 영역에 할당된 메모리는 모든
객체가 공유하여 하나의 멤버를 어디서든지 참조할 수 있는 장점이 있다.)

아래 예시를 보자.

package review;

public class Review1 {

	public static void main(String[] args) {
		Name.print(); // 인스턴스를 생성하지 않아도 호출이 가능

		Name name = new Name(); // 인스턴스 생성
		name.print2(); // 인스턴스를 생성하여야만 호출이 가능
	}

}

class Name {
	static void print() { // 클래스 메소드
		System.out.println("안녕하세요");
	}

	void print2() { // 인스턴스 메소드
		System.out.println("반갑습니다.");
	}
}

위 코드를 실행하면

이런 식으로 출력이 이루어지게 된다.

(따로 인스턴스를 생성하는것이 아니기에 바로 사용가능)

기존에 배운 내용은 다른 클래스에서 생성한 메소드를 사용하기 위해서는 메인 클래스에서 인스턴스를 생성하여야만 사용이 가능했지만,

static 정적을 이용하여 별도의 인스턴스 생성이 없이도 곧바로 다른 클래스의 메소드를 사용할 수 있게 되었다.

결과적으로 코드도 간략해지고, 메모리 자원도 아낄 수 있는 효과를 기대할 수 있겠다.


제너릭(Generic)

@Getter, @Setter, @ToString 없이 수행하기

제네릭(Generic)은 클래스 내부에서 지정하는 것이 아니라, 외부에서 사용자에 의해 지정되는 것을 의미한다.

이를 이용하여 이번에는 수업에서는 그동안 만들었던 @Getter, @Setter, @ToString 과 같은 어노테이션을 추가하지 않고 타 클래스에서 직접 수행을 할 수 있도록 '제너릭 클래스' 를 직접 만들어 보려고 한다.

int a = 10;
String b = null;

변수명에 타입을 고정하는 위와같은 기존의 방식들은직접 필요한만큼 변수들을 더 만들어야 했었다.

package day5;

// 기존의 Student, Book 클래스 가지고 와서 사용 할 예정.

// 제너릭 클래스
public class Exam<T> {

//  타입을 설정한다음에 클래스로 사용하겠다. 
//  (<T>는 정해져 있지 않음! -> T는 Main2.java에서 직접 정할 예정.)
	
	private T element; // <T>를 그대로 받음

	// public은 반환타입.
	public T getElement() { // @Getter 역활
		return element;
	}

	public void setElement(T element) { // @Setter 역활
		this.element = element;
	}
}

이렇게 @Getter, @Setter역할을 수행해 줄 제너릭 클래스를 만들어 둔 뒤

주석에서 이야기한대로 Main2 메인 클래스에서 T 값을 직접 정해보겠다.

package day5;

public class Main2 {

	public static void main(String[] args) {
                            /*---------------문자--------------*/
		// 클래스명 객체명 = new 생성자명();
		Exam <String> obj = new Exam</* 앞에서 String을 선언했기에 굳이 적지 않아도 됨. */>();
		obj.setElement("aaa");
		String str = obj.getElement();
		System.out.println(str);

                            /*---------------숫자--------------*/
		// 제너릭 클래스의 타입은 원시타입 대입 불가능! ex)int, long ※※※※중요!!※※※※
		Exam <Integer> obj1 = new Exam<Integer>(); 
        // int는 Integer, long은 Long 등으로 해야 함 (원시타입은 강사님 블로그 강의자료 상단에서 확인이 가능.)
		obj1.setElement(1000);
		System.out.println(obj1.getElement());
	}

}

Exam< T > 에서 T의 자리에 문자열과 숫자열을 각각을 넣어서

결과가 도출될 수 있도록 코드를 구성한 모습이다.
결과는 아래처럼 도출된다.

한가지 주의해야 할 점은 <> 안에 들어가는 타입은

원시타입 (ex : int, long, float 과 같이 앞글자가 소문자인 타입들) 은 대입할 수 없다는 점이다. 제너릭 클래스를 사용할 수 있는 타입은


이렇게 정리해 볼 수 있겠다.

이는 배열에서도 똑같이 적용이 가능한데,

package day5;

import java.util.ArrayList;

public class Main3 {

	public static void main(String[] args) {
		/*--------------------------------------------------------------------------------*/

		// 배열은 원시타입, Object타입 다 가능함.
		// Object타입은 초기값이 null로 된다.
        // int[] a = new int[5]; // 배열 5개가 만들어짐. 초기값은 0
        // int[] b = { 1, 2, 3, 4, 5 }; // 5개 만들어짐. 초기값들은 지정된 상태
        // String c[] = new String[5]; // 5개 만들어짐. 초기값은 null
        // Integer d[] = new Integer[5]; // 5개 만들어짐, 초기값 null

		/*--------------------------------------------------------------------------------*/
		// 컬렉션 | 배열의 갯수를 모름. 가변배열 ...
		ArrayList<Integer> e = new ArrayList<Integer>(); // e 라는 행렬 추가 : 초기값 []
		e.add(100);
		e.add(200);
		e.add(300);
		e.add(400);
		System.out.println(e); // 배열 [] 상태로 출력하기 : [100, 200, 300, 400]
		e.remove(2); // 3번째 항목 삭제하기
		System.out.println(e.size()); // 배열 크기 출력 : 3 
		System.out.println(e); // 그상태로 배열 [] 출력 : [100, 200, 400]

		// 배열 출력하기
		for (int i = 0; i < e.size(); i++) { // 0 1 2
			System.out.println(e.get(i)); // 배열 항목들을 하나하나 줄바꿈 출력
		}
		// 역순으로 출력하기
		for (int i = e.size() - 1; i >= 0; i--) { // 2 1 0
			System.out.println(e.get(i)); // 다시 하나하나 줄바꿔서 출력
		}

	}
}

위처럼 가변배열을 만들어서 직접 배열의 항목들을 추가 할 수 있겠다.


profile
시작은 미약하지만, 그 끝은 창대하리라

0개의 댓글