본문 바로가기
Java

[점프 투 자바] 7장 자바 날개달기1

by seonggu 2024. 1. 1.

패키지

패키지(package)는 비슷한 성격의 클래스들을 모아놓은 자바의 디렉터리이다.

 

  • 패키지 만들기

src 디렉터리 위에 마우스 오른쪽 버튼을 클릭해 [New → Package]를 선택한다.

패키지명 입력.

해당 패키지로 이동하여 클래스들을 생성하자.

만약 house 패키지를 생성했다면 class에는 다음과 같은 문장이 자동으로 삽입된다.

package house;

public class HouseKim {
}

 

  • 서브 패키지란?

기본 패키지 안에 존재하는 하위 패키지이다.

이를 사용해 기본 패키지 내의 클래스들을 분류하여 체계적으로 관리하고,

가독성을 향상시킬 수 있다.

house 패키지 위에 마우스 오른쪽 버튼을 클릭해 [New → Package]를 선택하여

house.person이라는 서브 패키지를 만들어보자.

여기에 SeongGu 클래스를 생성해보자

경로 : house/person/SeongGu.java

package house.person;

public class SeongGu {
}

패키지는 도트를 이용하여 서브 패키지를 표시한다.

 

  • 패키지 사용하기

다른 클래스에서 SeongGu 클래스를 사용하려면 다음과 같이 import를 해야한다.

import house.person.SeongGu;
import house.*; 

(*) 기호를 사용해서 house 패키지 내의 모든 클래스를 사용할 수 있다.

 

 

 

접근 제어자

접근 제어자(access modifier)를 사용하여 변수나 메서드의 사용 권한을 설정할 수 있다.

다음과 같은 접근 제어자를 사용하여 사용 권한을 설정할 수 있다.

  1. private
  2. default
  3. protected
  4. public

접근 제어자는 private < default < protected < public 순으로 보다 많은 접근을 허용한다.

 

  • private

접근 제어자가 private으로 설정되었다면

private이 붙은 변수나 메서드는 해당 클래스 안에서 접근이 가능하다.

public calss Sample {
	private String secret;
	private String getSecret(){
		return this.secret;
	}
}

secret 변수와 getSecret() 메서드는 오직 Sample 클래스에서만 접근이 가능하다.

다른 클래스에서 접근 불가능

 

  • default

접근 제어자를 별도로 설정하지 않았다면 변수나 메서드는 default 접근 제어자가 자동으로 설정되어

동일한 패키지 안에서만 접근이 가능하다.

house/SeongGu.java

package house;  // 패키지가 동일하다.

public class SeongGu {
    String lastname = "Do";  // lastname은 default 접근제어자로 설정된다.
}

 

house/SeongGuKim.java

package house;  // 패키지가 동일하다.

public class SeongGuKim {
    String lastname = "Kim"; 

    public static void main(String[] args) {
				SeongGu sg = new SeongGu();
        System.out.println(sg.lastname);  // HouseKim 클래스의 lastname 변수를 사용할 수 있다.
    }
} // DO 출력

둘의 패키지는 house로 동일하다. 따라서 SeongGuKim클래스에서 default 접근 제어자로 설정된 SeongGu의 lastname 객체 변수에 접근이 가능하다.

 

  • protected

protected가 붙은 변수나 메서드는 동일 패키지의 클래스 또는 해당 클래스를 상속받은 클래스에서만 접근이 가능하다.

 

house/SeongGu.java

package house;  // 패키지가 동일하다.

public class SeongGu {
    protected String lastname = "Do";  // lastname은 default 접근제어자로 설정된다.
}

 

house/person/SeongGuKim.java

package house.person;

import house.SeongGu;

public class SeongGuKim extends SeongGu { // SeongGu 상속
	public static void main(String[] args){
				SeongGuKim sgk = new SeongGuKim();
				System.out.println(sgk.lastname); // 상속한 클래스의 protected 변수는 접근가능

	}
} // Do 출력

SeongGu클래스를 상속한 SeongGuKim 클래스의 패키지가 서로 다르지만

SeongGu의 lastname 접근 제어자가 protected이기 때문에 sgk.lastname으로 접근이 가능하다.

lastname 접근 제어자가 protected가 아닌 default였다면 sgk.lastname은 컴파일 오류가 날 것이다.

 

  • public

public으로 설정되어 있다면 public 접근 제어자가 붙은 변수나 메서드는 어떤 클래스에서도 접근이 가능하다.


요약

private : 동일 클래스까지

default : 동일 패키지 내까지

protected : 상속한 받은 클래스, 동일 패키지의 클래스까지

public : 어떤 클래스던 가능

 

 

스태틱

스태틱(static)은 클래스에서 공유되는 변수나 메서드를 정의할 때 사용된다.

 

  • static 변수
class HouseLee {
    String lastname = "이";
}

public class Sample {
    public static void main(String[] args) {
        HouseLee lee1 = new HouseLee();
        HouseLee lee2 = new HouseLee();
    }
}

HouseLee 클래스를 만들고 객체를 생성하면 객체마다 객체 변수 lastname을 저장하기 위한 메모리가 별도로 할당된다.

생각해보면 HouseLee 클래스의 lastname은 어떤 객체이든지 동일한 값인 ‘이’이어야 할 것 같다.

 

이렇게 항상 값이 변하지 않으면 static을 사용하여 메모리 낭비를 줄여보자

class HouseLee {
    static String lastname = "이";
}

public class Sample {
    public static void main(String[] args) {
        HouseLee lee1 = new HouseLee();
        HouseLee lee2 = new HouseLee();
    }
}

lastname 변수에 static 키워드를 붙이면 자바는 메모리 할당을 딱 한번만 하게 되어 메모리를 적게 사용할 수 있다.

만약 값도 변경되지 않으면 final 키워드를 붙이자

 

static을 사용하는 두 번째 이유는 값을 공유할 수 있기 때문이다. static으로 설정하면 같은 메모리 주소만을 바라보기 때문에 static 변수의 값을 공유하게 되는 것이다.

 

예제 살펴보기

class Counter  {
    int count = 0;
    Counter() {
        this.count++;
        System.out.println(this.count);
    }
}

public class Sample {
    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
    }
}

 

Sample 클래스를 실행하면 다음과 같은 결과값이 나옴

1
1

객체 c1, c2를 생성할 때 객체 변수인 count의 값을 1씩 증가시키더라도

c1, c2와 count는 서로 다른 메모리를 가리키고 있기 때문에 원하던 결과가 나오지 않는다.

객체 변수는 항상 독립적인 값을 갖기 때문에 당연한 결과이다.

 

두 번째 예제

class Counter  {
    static int count = 0;
    Counter() {
        count++;  // count는 더이상 객체변수가 아니므로 this를 제거하는 것이 좋다.
        System.out.println(count);  // this 제거
    }
}

public class Sample {
    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
    }
}

int count = 0 앞에 static을 붙였더니 count 값이 공유되어 count가 증가되어 출력된다.

1
2

보통 변수에 쓰는 static 키워드는 프로그래밍을 할 때 메모리의 효율을 높이기 위한 목적보다는

공유의 목적으로 훨씬 더 많이 사용된다.

 

  • static 메서드

static이라는 키워드가 메서드 앞에 붙으면 이 메서드는 스태틱 메서드(static method)가 된다.

class Counter  {
    static int count = 0;
 
   Counter() {
        count++;
        System.out.println(count);
    }

    public static int getCount() {
        return count;
    }
}

public class Sample {
    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();

        System.out.println(Counter.getCount());  // 스태틱 메서드는 클래스를 이용하여 호출
    }
}

Counter 클래스에 getCount()라는 스태틱 메서드를 추가했다.

static 키워드를 붙이면 Counter.getCount() 와 같이 객체 생성 없이도 클래스를 통해 메서드를 직접 호출할 수 있다.

 

스태틱 메서드 안에서는 객체 변수 접근이 불가능하다.

이 예제에서는 count 변수가 static 변수이기 때문에 스태틱 메서드에서 접근이 가능하다.

 

스태틱 메서드는 유틸리티성 메서드를 작성할 때 많이 사용된다.

예를 들어 ‘오늘의 날짜 구하기’, ‘숫자에 콤마 추가하기’ 등의 메서드를 작성할 때 스태틱 메서드를 사용하는 것이 유리하다.

 

✅ 유틸리티성 메서드는 특정 클래스나 인스턴스에 종속되지 않고, 재사용이 가능하고 범용 기능을 제공하는 스태틱 메서드를 말한다.

이 메서드들은 코드의 중복을 줄이고 가독성을 향상시킨다.

 

오늘의 날짜를 구하는 Util 클래스의 예제

import java.text.SimpleDateFormat;
import java.util.Date;

class Util {
    public static String getCurrentDate(String fmt) {
        SimpleDateFormat sdf = new SimpleDateFormat(fmt);
        return sdf.format(new Date());
    }
}

public class Sample {
    public static void main(String[] args) {
        System.out.println(Util.getCurrentDate("yyyyMMdd"));  // 오늘 날짜 출력
    }
}

 

 

싱글톤 패턴

자바의 디자인 패턴 중 하나인 싱글톤(singleton)을 알아보자.

static에 대한 개념이 생겼기 때문에 싱글톤을 이해하기 수월해진다.

 

싱글톤은 단 하나의 객체만들 생성하게 강제하는 디자인 패턴이다.

다시 말해, 클래스를 통해 생성할 수 있는 객체가 한 개만 되도록 만드는 것이 싱글톤이다.

 

✅ 디자인 패턴은 소프트웨어 설계에서 반복적으로 나타나는 문제들을 효과적으로 해결하는 데 사용되는 검증도딘 설계 방법론이다.

 

예제

class Singleton {
    private Singleton() {
    }
}

public class Sample {
    public static void main(String[] args) {
        Singleton singleton = new Singleton();  // 컴파일 오류가 발생한다.
    }
}

컴파일 오류 발생

private 접근 제어자를 사용하면 다른 클래스에서 Singleton 클래스의 생성자로 접근을 막았기 때문이다.

이렇게 생성자를 private으로 만들어버리면 Singleton 클래스를 다른 클래스에서 new를 이용하여 생성할 수 없게 된다.

 

new를 이용하여 무수히 많은 객체를 생성하면 싱글톤의 정의에 벗어나므로 일단 new로 객체생성을 막았다.

 

그렇다면 Singleton 클래스의 객체는 어떻게 생성할 수 있을까?

class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return new Singleton();  // 같은 클래스이므로 생성자 호출이 가능하다.
    }
}

public class Sample {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
    }
}

getInstance() 라는 스태틱 메서드를 이용하여 Singleton 클래스의 객체를 생성할 수 있다.

하지만 getInstance()를 호출할 때마다 새로운 객체가 생성되므로

싱글톤이 아니다.

 

다시 수정해보자.

class Singleton {
    private static Singleton one;
    private Singleton() {
    }

    public static Singleton getInstance() {
        if(one==null) {
            one = new Singleton();
        }
        return one;
    }
}

public class Sample {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);  // true 출력
    }
}

Singleton 클래스에 one이라는 static 변수를 작성하고,

getInstance 메서드에 one값이 null인 경우에만 객체를 생성하게 하여 one 객체가 딱 한 번만 만들어지게 했다.

다시 보면.

 

처음 getInstance가 호출되면 one이 null이므로 new에 의해서 one 객체가 생성된다.

이렇게 한 번 생성되면 one은 static 변수이기 때문에 그 이후로는 null이 아니다.

이어서 다시 getlnstance 메서드가 호출되면 이미 만들어진 싱글톤 객체인 one을 항상 리턴한다.

 

main 메서드에서 getInstance를 두 번 호출하여 각각 얻은 객체가 같은 객체인지 조사해보았다.

프로그램을 실행하면 예상대로 true가 출력되어 같은 객체임을 확인할 수 있다.

 

 

예외처리

프로그램을 만들다 보면 수없이 많은 예외 상황이 발생한다.

예외를 처리하기 위해서 try ~ catch, throws 구문을 이용해보자.

예외 처리 방법을 알게 되면 보다 안전하고 유연한 프로그래밍을 구사할 수 있다.

 

  • 예외는 언제 발생하는가?

다음과 같이 ‘나없는파일’이라는 이름의 존재하지 않는 파일을 열기위한 자바 프로그램을 작성해보자.

import java.io.*;

public class Sample {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader("나없는파일"));
        br.readLine();
        br.close();
    }
}

코드를 실행하면 존재하지 않는 파일을 열려고 시도했기 때문에 원하는 파일을 찾을 수 없다는 FileNotFoundException이라는 예외가 발생한다.

Exception in thread "main" java.io.FileNotFoundException: 나없는파일 (지정된 파일을 찾을 수 없습니다)
    at java.io.FileInputStream.open(Native Method)
    at java.io.FileInputStream.<init>(Unknown Source)
    at java.io.FileInputStream.<init>(Unknown Source)
    at java.io.FileReader.<init>(Unknown Source)
    ...

 

이번에는 0으로 다른 숫자를 나누는 프로그램

public class Sample {
    public static void main(String[] args){
        int c = 4 / 0;
    }
}
//Exception in thread "main" java.lang.ArithmeticException: / by zero
//  at Test.main(Test.java:14)

4를 0으로 나눌 수 없으므로 산술에 문제가 생겼다는 ArithmeticException 예외가 발생한다.

 

마지막으로

public class Sample {
    public static void main(String[] args) {
        int[] a = {1, 2, 3};
        System.out.println(a[3]);
    }
}

//Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
//    at Test.main(Test.java:17)

a[3]은 a 배열의 4번째 값으로 a 배열에서 구할 수 없다.

그래서 배열에서 아무것도 없는 곳을 가리켰다는 ArrayIndexOutOfBoundsException발생했다.

 

  • 예외 처리하기

예외들을 처리하기 위해 try ~ catch문을 사용해보자

일단 기본 구조를 살펴보기

try {
    <수행할 문장 1>;
    <수행할 문장 2>;
    ...
} catch(예외1) {
    <수행할 문장 A>;
    ...
} catch(예외2) {
    <수행할 문장 a>;
    ...
}

try 문의 수행할 문장 중 예외가 발생하지 않다면 catch 문에 속한 문장들은 수행되지 않는다.

하지만 try 문 안의 문장을 수행하는 도중에 예외가 발생하면 예외에 해당되는 catch문이 수행된다.

 

숫자를 0으로 나누었을 때 발생하는 예외를 처리하면 다음과 같이 할 수 있다.

public class Sample {
    public static void main(String[] args) {
        int c;
        try {
            c = 4 / 0;
        } catch(ArithmeticException e) {
            c = -1;  // 예외가 발생하여 이 문장이 수행된다.
        }
    }
}

ArithmeticException이 발생하면 c에 -1을 대입하도록 예외처리를 한 것이다.

ArithmeticException e 에서는 e는 ArithmeticException 클래스의 객체

즉, 예외 객체에 해당한다. 이 예외 객체를 통해 예외 클래스의 변수나 메서드를 호출할 수도 있다.

 

  • finally

프로그램 수행 도중 예외가 발생하면 프로그램이 중지되거나

예외 처리에 의해 catch 구문이 실행된다.

하지만 어떤 예외가 발생하더라도 반드시 실행해야한다면 finally를 사용해보자

public class Sample {
    public void shouldBeRun() {
        System.out.println("ok thanks.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;
        try {
            c = 4 / 0;
            sample.shouldBeRun();  // 이 코드는 실행되지 않는다.
        } catch (ArithmeticException e) {
            c = -1;
        }
    }
}

여기서 sample.shouldBeRun()는 절대로 실행될 수 없다.

왜냐하면 4/0에 의해 ArithmeticException이 발생하여 catch 구문으로 넘어가기 때문이다.

 

반드시 sample.shouldBeRun() 메서드가 반드시 실행되어야 한다면 처리하기 위해

자바에서는 다음과 같이 finally 문을 사용한다.

public class Sample {
    public void shouldBeRun() {
        System.out.println("ok thanks");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        int c;
        try {
            c = 4 / 0;
        } catch (ArithmeticException e) {
            c = -1;
        } finally {
            sample.shouldBeRun();  // 예외에 상관없이 무조건 수행된다.
        }
    }
}

finally 문은 try 문장 수행 중 예외 발생 여부에 상관없이 무조건 실행된다.

따라서 코드를 실행하면 sample.shouldBeRun() 메서드가 수행되어 ‘ok, thanks’라는 문장이 출력된다.

 

  • 예외 활용하기 - RuntimeException과 Exception

다음 예제를 살펴보자

public class Sample {
    public void sayNick(String nick) {
        if("바보".equals(nick)) {
            return;
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("바보");
        sample.sayNick("야호");
    }
}

바보라는 문자열이 입력되면 return으로 메서드를 종료하여 별명이 출력되지 못한다.

 

1) RuntimeException

“바보” 문자열이 입력되면 return으로 메서드를 빠져나가지 않고 적극적으로 예외를 발생시켜보자.

그러기위해 아래 클래스를 추가해보자.

class FoolException extends RuntimeException {// 클래스 추가 
} 

public class Sample {
    public void sayNick(String nick) {
        if("바보".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("바보");
        sample.sayNick("야호");
    }
}

return했던 부분을 throw new FoolException() 이라는 문장으로 변경하였다.

이 프로그램을 실행하면 ‘바보’라는 입력값으로 sayNick 메서드 실행 시 다음과 같은 예외가 발생한다.

Exception in thread "main" FoolException
    at Sample.sayNick(Sample.java:7)
    at Sample.main(Sample.java:14)

FoolException이 상속받은 클래스는 RuntimeException이다.

 

예외는 크게 두가지로 구분된다.

  1. RuntimeException : 실행 시 발생하는 예외
  2. Exception : 컴파일 시 발생하는 예외

Exception은 예측이 가능한 경우 사용하고

RuntimeException은 발생할 수도 있고 발생하지 않을 수도 있는 경우 사용한다.

 

Exception → Checked Exception

RuntimeException → Unchecked Exception이라고도 한다.

 

  • Exception

FoolException 클래스를 다음과 같이 변경해보기

RuntimeException을 Exception 상속으로 변경

그런데 이렇게 하면 Sample 클래스에서 컴파일 오류가 발생한다.

 

FoolException이 예측 가능한 Checked Exception으로 변경되어 예외 처리를 컴파일러가 강제하기 때문이다. 따라서 다음과 같이 변경해야 정상적으로 컴파일이 된다.

class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) {
        try {
            if("바보".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("바보");
        sample.sayNick("야호");
    }
}

이와 같이 컴파일 오류를 막기 위해서는 sayNick 메서드에서 try ~ catch 문으로 FoolException을 처리해야한다.

 

  • 예외 던지기

앞 예제에서는 sayNick 메서드에서 FoolException을 발생시키고 예외 처리도 sayNick 메서드에서 했다.

하지만 이렇게 하지 않고 sayNick을 호출한 곳에서

FoolException을 처리하도록 예외를 위로 던질 수 있다.

public class Sample {
    public void sayNick(String nick) throws FoolException {
        try {   // try .. catch 문을 삭제할수 있다.
            if("바보".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 "+nick+" 입니다.");
        }catch(FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("바보");
        sample.sayNick("야호");
    }
}

sayNick 메서드 뒷부분에 throws라는 구문을 이용하여 FoolExcpetion을 위로 보낼 수 있다.

이를 예외를 뒤로 미루기 라고 한다.

 

🔴 throw와 throws의 차이

throw와 throws는 예외 처리와 관련된 키워드로 다음과 같은 차이점이 있다.

throw : 메서드 내에서 예외를 발생시키는데 사용한다. (예: throw new FoolException())

throws : 메서드 선언부에서 사용되며, 해당 메서드가 처리하지 않은 예외를 호출자에게 전달함을 나타낸다.

(예: public void sayNick(String nick) throws FoolException)

 

위와 같이 sayNick 메서드를 변경하면 main 메서드에 컴파일 오류가 발생한다.

throws 구문 때문에 FoolException의 예외를 처리해야 하는 대상이

sayNick 메서드에서 main메서드 (sayNick을 호출하는 메서드)로 변경 되었기 때문이다.

 

컴파일 오류를 해결하려면 이번엔 main 메서드를 변경하자.

class FoolException extends Exception {
}

public class Sample {
    public void sayNick(String nick) throws FoolException {
        if("바보".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        try {
            sample.sayNick("바보");
            sample.sayNick("야호");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }
}
FoolException이 발생했습니다.

main 메서드에서 try ~ catch 문으로 sayNick 메서드에 대한 FoolException예외 처리를 했다.

 

그럼 FoolException 처리를 sayNick 메서드에서 하는 것이 좋을까?

아니면 throws를 이용하여 예외 처리를 main 메서드에서 하는 것이 좋을까?

 

sayNick 메서드에서 처리하는 것과 main메서드에서 처리하는 것에는 아주 큰 차이가 있다.

sayNick 메서드에서 예외를 처리하는 경우에는 다음의 두 문장이 모두 수행된다.

sample.sayNick("바보");
sample.sayNick("야호");

물론 sample.sayNick(”바보”); 문장 수행 시에는 FoolException이 발생하지만

그 다음 문장인 sample.sayNick(”야호”); 역시 수행된다.

 

하지만 main 메서드에서 예외 처리를 한 경우에는 두번 째 문장인

sample.sayNick(”야호”); 가 수행되지 않는다.

이미 첫 번째 문장에서 예외가 발생하여 catch문으로 빠져버리기 때문이다.

try {
    sample.sayNick("바보");
    sample.sayNick("야호");  // 이 문장은 수행되지 않는다.
}catch(FoolException e) {
    System.err.println("FoolException이 발생했습니다.");
}

 

이런 이유로 프로그래밍할 때 예외 처리를 하는 위치는 매우 중요하다.

프로그램의 수행 여부를 결정하기도 하고 트랜잭션 처리와도 밀접한 관계가 있기 때문이다.

 

  • 트랜잭션

트랜잭션과 예외처리는 매우 밀접한 관련이 있다.

트랜잭션과 예외처리가 서로 어떤 관련이 있는지 알아보자.

트랜잭션 : 하나의 작업 단위

 

예를 들어 쇼핑몰의 ‘상품발송’ 이라는 트랜잭션을 가정하면

‘상품발송’ 에는 다음과 같은 작업들이 있다.

포장, 영수증 발행, 발송

쇼핑몰의 운영자는 이 3가지 일 중 하나라도 실패하면 3가지 모두 취소하고 ‘상품발송’ 전의 상태로 되돌리고 싶어 한다.

 

모두 취소하지 않으면 ??

데이터의 정합성이 크게 흔들리게 된다. 이렇게 모두 취소하는 행위를 롤백(rollback)이라 한다.

데이터 정합성이란 간단하게 데이터들의 값이 서로 일관성 있게 일치하는 것을 의미한다.

 

슈도코드로 알아보자.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장() {
   ...
}

영수증발행() {
   ...
}

발송() {
   ...
}

다시 정리하면 쇼핑몰 운영자는 ‘포장’, ‘영수증발행’, ‘발송’이라는 3가지 작업 중 1개라도 실패하면

모든 작업을 취소하고 싶어 한다.

이런 경우는 어떻게 처리할까??

 

다음과 같이 세가지 메서드에서는

각각 예외를 던지고 상품발송 메서드에서

던져진 예외를 처리한 뒤 모두 취소하는 것이 완벽한 트랜잭션 처리 방법이다.

상품발송() {
    try {
        포장();
        영수증발행();
        발송();
    }catch(예외) {
        모두취소();  // 하나라도 실패하면 모두 취소한다.
    }
}

포장() throws 예외 {
   ...
}

영수증발행() throws 예외 {
   ...
}

발송() throws 예외 {
   ...
}

이와 같이 코드를 작성하면 세 개의 단위 작업 중 하나라도 실패할 경우 예외가 발생되어

상품발송이 모두 취소된다.

 

다음은 옳지 못한 방법이다 눈으로만 확인하자.

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장(){
    try {
       ...
    }catch(예외) {
       포장취소();
    }
}

영수증발행() {
    try {
       ...
    }catch(예외) {
       영수증발행취소();
    }
}

발송() {
    try {
       ...
    }catch(예외) {
       발송취소();
    }
}

이렇게 각각 메서드에서 예외가 처리된다면 포장 메서드는 실행되었는데

발송 메서드는 실행되지 않고 뒤죽박죽 상황이 연출될 것이다.

 

실제 프로젝트에서도 이와 같이 트랜잭션 관리를 잘못하여 고생하는 경우를 많이 본다고한다.

그것은 재앙에 가깝다.

 

프로그래머의 실력을 평가할 때 예외 처리를 어떻게 하고 있는지를 보면

그 사람의 실력을 어느 정도 가늠해 볼 수 있다고 한다.

코드의 특정 부분만을 알아서는 안되고 코드 전체뿐만 아니라 코드의 동작 과정이나 흐름등을 모두 알아야만 예외 처리를 정확히 할 수 있기 때문이다.

 

 

스레드

동작하고 있는 프로그램을 프로세스(process)라고 한다.

보통 한 개의 프로세스는 한 가지의 일을 하지만,

스레드(thread)를 이용하면 한 프로세스 내에서 두가지 또는 그 이상의 일을 동시에 할 수 있다.

 

  • Thread

간단한 예제를 통해 알아보자.

public class Sample extends Thread {
    public void run() {  // Thread 를 상속하면 run 메서드를 구현해야 한다.
        System.out.println("thread run.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.start();  // start()로 쓰레드를 실행한다.
    }
}

Sample 클래스가 Thread 클래스를 상속했다.

Thread 클래스의 run 메서드를 구현하면 sample.start() 메서드를 실행할 때

sample 객체의 run메서드가 수행된다.

 

이 예제를 실행하면 ‘thread run’이라는 문장이 출력된다. 하지만 이와 같이 스레드가 하나인 경우

도대체 스레드가 어떻게 동작하고 있는지 명확하지 않다.

 

동작을 확인할 수 있게 예제를 수정해보자.

public class Sample extends Thread {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료 
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Sample(i);
            t.start();
        }
        System.out.println("main end.");  // main 메서드 종료
    }
}

총 10개의 스레드를 실행시키는 예제이다.

어떤 스레드인지 확인하기 위해서 스레드마다 생성자에 순서를 부여했다.

 

그리고 시작과 종료 출력을 햇고

시작과 종료사이에 1초간격이 생기도록 작성했다.

결과

0 thread start.
4 thread start.
6 thread start.
2 thread start.
main end.
3 thread start.
7 thread start.
8 thread start.
1 thread start.
9 thread start.
5 thread start.
0 thread end.
4 thread end.
2 thread end.
6 thread end.
7 thread end.
3 thread end.
8 thread end.
9 thread end.
1 thread end.
5 thread end.

0번부터 9번까지의 스레드는 순서대로 실행되지 않는다.

순서에 상관없이 실행된다는 사실을 알 수 있음.

또한, 스레드가 종료되기 전에 main 메서드가 종료되었다.

 

  • Join

앞선 예제를 보면 스레드가 모두 수행되고 종료되기 전에 main메서드가 먼저 종료되어 버렸다.

그럼 스레드가 모두 종료된 후에 main 메서드를 종료하고 싶은 경우에는?

import java.util.ArrayList;

public class Sample extends Thread {
    int seq;
    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for(int i=0; i<10; i++) {
            Thread t = new Sample(i);
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join(); // t 쓰레드가 종료할 때까지 기다린다.
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}

생성된 스레드를 담기 위해 ArrayList 객체인 threads를 만든 후 스레드 생성시

생성된 객체를 threads에 저장했다.

 

그리고 main 메서드가 종료되기 전에 threads 객체에 담긴 각각의 스레드에 join 메서드를 호출하여

스레드가 종료될 때 까지 대기하도록 했다.

join 메서드는 스레드가 종료될 때까지 기다리게하는 메서드이다.

0 thread start.
5 thread start.
2 thread start.
6 thread start.
9 thread start.
1 thread start.
7 thread start.
3 thread start.
8 thread start.
4 thread start.
0 thread end.
5 thread end.
2 thread end.
9 thread end.
6 thread end.
1 thread end.
7 thread end.
4 thread end.
8 thread end.
3 thread end.
main end.

스레드를 활용한 프로그래밍을 할 때 가장 많이 실수하는 부분이

스레드가 종료되지 않았는데 스레드가 종료된 줄 알고 다음 작업을 진행하게 만드는 일이다.

스레드가 모두 종료된 후 그 다음 작업을 진행해야할 때 join 메서드를 사용하자

 

  • Runnable

Thread 객체를 만들 때 앞서 살펴본 예처럼 Thread 클래스를 상속하여 만들기도 하지만 주로 Runnable 인터페이스를 사용한다.

왜냐하면 Thread 클래스를 상속하면 Thread 클래스를 상속한 클래스가 다른 클래스를 상속할 수 없기 때문이다. (자바에서는 다중상속 금지)

 

Runnable 인터페이스를 사용하는 방식으로 변경하기.

import java.util.ArrayList;

public class Sample implements Runnable {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq+" thread start.");
        try {
            Thread.sleep(1000);
        }catch(Exception e) {
        }
        System.out.println(this.seq+" thread end.");
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for(int i=0; i<10; i++) {
            Thread t = new Thread(new Sample(i));
            t.start();
            threads.add(t);
        }

        for(int i=0; i<threads.size(); i++) {
            Thread t = threads.get(i);
            try {
                t.join();
            }catch(Exception e) {
            }
        }
        System.out.println("main end.");
    }
}

Thread 클래스를 extends하던 것에서 Runnable 인터페이스를 implements하도록 변경했다.

 

Thread 객체를 생성하는 부분을 변경했다.

Thread t = new Thread(new Sample(i));

Thread 객체의 생성자로 Runnable 인터페이스를 구현한 객체를 전달하는 방법을 사용함.

이렇게 변경된 코드는 이전에 만들었던 예제와 완전히 동일하게 동작함.

 

Thread객체가 Thread 클래스를 상속했을 경우 다른 클래스를 상속할 수 없지만.

인터페이스를 사용한 경우 다른 클래스 상속이 가능하므로 좀 더 유연한 프로그램을 만들 수 있다.

 

 



참고자료 :
위키독스 점프 투 자바 https://wikidocs.net/book/31