자바의 Etc

포모·2020년 12월 2일
1

JAVA의 기본

목록 보기
5/9

🎯 접근 제어자

private

private이 붙은 변수, 메소드는 해당 클래스에서만 접근이 가능합니다.

public class AccessModifier {
    private String secret;
    private String getSecret() {
        return this.secret;
    }
}

secret 변수와 getSecret 메소드는 오직 AccessModifier 클래스에서만 접근이 가능하고 다른 클래스에서는 접근이 불가능합니다.


default

접근제어자를 별도로 설정하지 않는다면 default 접근제어자가 되어 해당 패키지 내에서만 접근이 가능합니다.


protected

protected가 붙은 변수, 메소드는 동일 패키지 내의 클래스 또는 해당 클래스를 상속받은 외부 패키지의 클래스에서 접근이 가능합니다.


public

public 접근제어자가 붙은 변수, 메소드는 어떤 클래스에서라도 접근이 가능합니다.


🎯 Static

static 변수

public class HousePark  {
    static String lastname = "박";

    public static void main(String[] args) {
        HousePark pey = new HousePark();
        HousePark pes = new HousePark();
    }
}

위와 같은 코드에서 HousePark은 어떤 객체가 생성되어도 항상 "박"이라는 동일한 값을 가집니다.
lastnamestatic 키워드를 붙이면 자바는 메모리 할당을 딱 한번만 하게 되어 메모리 사용에 이점을 보입니다.
이말인 즉슨 static으로 설정하면 같은 곳의 메모리 주소만을 바라보기 때문에 static 변수의 값을 공유하게 됩니다.

만약 lastname의 값이 변경되지 않기를 바란다면 static 키워드 앞에 final이라는 키워드를 붙이면 됩니다.
final을 붙이면 그 값을 변경하지 못하게 하는 기능이 있습니다.


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

    public static void main(String[] args) {
        Counter c1 = new Counter();		// 1
        Counter c2 = new Counter();		// 2
    }
}

위와 같이 countstatic으로 설정하면 count 값이 공유되어 객체가 생성될 때마다 count가 올라갑니다.


static method

static 키워드가 붙은 메소드를 static method라고 합니다.

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


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

    public static void main(String[] args) {
        System.out.println(Util.getCurrentDate("yyyyMMdd"));
    }
}

getCurrentData라는 static method를 이용하여 오늘 날짜를 바로 구할 수 있습니다.


Singleton pattern

싱글톤은 단 하나의 객체만을 생성하게 강제하는 패턴입니다.
즉, 클래스를 통해 생성할 수 있는 객체는 한 개만 되도록 하는 것입니다.

class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return new Singleton();
    }
}

public class SingletonTest {
    public static void main(String[] args) {
    	 Singleton singleton = new Singleton();		// error
        Singleton singleton = Singleton.getInstance();
    }
}

위와 같이 생성자를 private으로 작성하면 컴파일 에러가 발생합니다.
그렇기에 getInstance라는 static 메소드를 이용하여 싱글톤 객체를 돌려받을 수 있습니다.
그러나 이렇게 작성하면 getInstance가 호출될 때마다 새로운 객체가 생성되는 것이기 때문에 싱글톤이 아닙니다.

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

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

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

Singleton 클래스에 one이라는 변수를 두고 getInstance 메소드에서 one값이 null인 경우에만 객체를 생성하도록하여 객체가 한번만 생성되도록 해주었습니다.

getInstance가 호출된 두 개의 객체가 서로 같은지 확인해보면 true를 출력하는 것을 확인할 수 있었습니다.


🎯 Exception

예외 처리하기

try {
    ...
} catch(예외1) {
    ...
} catch(예외2) {
    ...
...
}

예외처리를 위해서 try catch문의 기본은 위와 같습니다.
프로그램 도중 예외가 발생하면 프로그램이 중지되거나 예외처리를 했을 경우 catch 구문이 실행됩니다.


int c;
try {
    c = 4 / 0;
}catch(ArithmeticException e) {
    c = -1;
}

숫자를 0으로 나누었을 때 발생하는 예외를 처리하려면 ArithmeticExceptioncatch 문에 입력해주어야합니다.
위 코드는 예외가 발생할 경우 c-=1을 실행합니다.


finally

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

    public static void main(String[] args) {
        Test test = new Test();
        int c;
        try {
            c = 4 / 0;
        } catch (ArithmeticException e) {
            c = -1;
        } finally {
            test.shouldBeRun();
        }
    }
}

finally는 어떤 예외가 발생하더라도 반드시 실행되어야하는 부분이 있을 때 사용됩니다.
위와 같은 코드는 ArithmeticException가 발생해도 finally 안에 있는 test.shouldBeRun()가 실행됩니다.


예외 발생시키기 (throw, throws)

RuntimeException

FoolException.java
public class FoolException extends RuntimeException {
}

Test.java
public class Test {
    public void sayNick(String nick) {
        if("fool".equals(nick)) {
            throw new FoolException();		---- (1)
        }
        System.out.println("당신의 별명은 "+nick+" 입니다.");
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.sayNick("fool");
        test.sayNick("genious");
        
        /* 실행 결과
        Exception in thread "main" FoolException
    	at Test.sayNick(Test.java:7)
    	at Test.main(Test.java:15)	*/
    }
}
  • (1) : "fool"이라는 입력값으로 sayNick 메소드를 실행 시 FoolException()을 발생시킵니다.

FoolException이 상속받은 클래스는 RuntimeException입니다. Exception은 크게 두가지로 구분되는데요.

  1. RuntimeException : 실행 시 발생하는 예외

    • 예외가 발생할 수도 안 할수도 있는 경우에 사용합니다.
    • UncheckedException 이라고 합니다.
  2. Exception : 컴파일 시 발생하는 예외

    • 프로그램 작성 시 이미 예측가능한 예외를 작성할 때 합니다.
    • 다른 말로 Checked Exception이라고 합니다.

Exception

public class FoolException extends Exception {
}

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

    public static void main(String[] args) {
        Test test = new Test();
        test.sayNick("fool");
        test.sayNick("genious");
    }
}

이번에는 FooleExceptionException을 상속하도록 변경했습니다.
예측 가능한 Checked Exception이기 때문에 예외처리를 컴파일러가 강제하게 되고, Test 클래스에서는 컴파일 오류가 발생할 것입니다.
(사용자로 하여금 입력을 받는 경우는 컴파일에서 예외를 예측할 수 없습니다.)

따라서 try..catch 문을 통해 FoolException을 처리해줘야합니다.


throws

위의 예제를 보면 sayNick 메소드에서 FoolException을 발생 시키고 catch 문에서도 예외처리를 해주었는데, throws를 통해 sayNick을 호출한 곳에서 FoolException을 처리하도록 할 수 있는 방법이 있습니다.

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

throws 라는 구문을 이용하여 FoolException을 위로 보낼 수 있습니다.

위와 같이 작성하면 throws 구문으로 인해 FoolException의 예외처리를 해야하는 대상이 sayNick 메소드에서 main 메소드(sayNick을 호출하는 메소드)로 변경되었기 때문입니다.


public static void main(String[] args) {
    Test test = new Test();
    try {
        test.sayNick("fool");
        test.sayNick("genious");
    }catch(FoolException e) {
        System.err.println("FoolException이 발생했습니다.");
    }
}

main 메소드는 try..catch 문을 통해 FoolException에 대한 예외처리를 해야합니다.


그렇다면 FoolException 처리를 sayNick에서 하는 것이 좋을까? 아니면 main에서 하는 것이 좋을까? 🤔


sayNick 메소드에서 예외처리를 하는 경우

test.sayNick("fool");
test.sayNick("genious");

다음의 두 문장이 모드 수행됩니다. 물론, `test.sayNick("fool");`에서 `FoolException`이 발생하겠지만 다음 문장도 실행되긴 합니다.

하지만 `main`에서 예외처리한 경우는 첫번째 문장에서 `catch` 문으로 빠져버리기때문에 두번째 문장이 실행되지 않습니다.

transaction

트랜잭션은 하나의 작업 단위를 의미합니다. 예외처리와 매우 밀접한 관련이 있습니다.

상품 발송에 대한 트랜잭션에는 아래와 같습니다.

  • 포장
  • 영수증 발행
  • 발송

만약 포장, 영수증 발행, 발송 중 하나라도 실패하면 모두 취소한다고 가정해봅시다. 이럴때 예외처리는 어떻게 할까요?

상품발송() {
	try {
	  포장();
	  영수증발행();
	  발송();
	} catch (예외) {
	  모두취소();
	}
}

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

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

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

만약, 상품발송()try..catch 문이 있는 것이 아닌 포장(), 영수증발행(), 발송()에 각각 try..catch 문이 있다면 발송은 됐는데 포장이 엉망이라던지 하는 상황이 발생할 수 있습니다.

이렇게 여러 트랜잭션에 유의하면서 적절한 방식을 찾아야합니다.


🎯 Thread

동작하고 있는 프로그램을 프로세스(process)라고 합니다.
보통 프로세스는 한가지의 일을 하지만 Thread를 이용하면 두 가지 이상의 일을 동시에 할 수 있습니다.


public class Test extends Thread {
    public void run() {
        System.out.println("thread run.");
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.start();
    }
}

Test 클래스는 Thread 클래스를 상속받았습니다.
Thread 클래스의 run 메소드를 Test 클래스 내에서 구현하고, test.start()를 통해 run 메소드를 실행할 수 있습니다.


public class Test extends Thread {
    int seq;
    public Test(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) {
        for(int i=0; i<10; i++) {
            Thread t = new Test(i);
            t.start();
        }
        System.out.println("main end.");
    }
}

총 10개의 thread를 만들고 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.

위의 출력값을 보면 thread 순서가 일정하지 않은 것을 볼 수 있습니다. 이를 통해 thread는 순서에 상관없이 동시에 실행된다는 사실을 알 수 있습니다.
중간에 main end가 출력되면서 main 메소드가 먼저 종료되는 것을 확인할 수도 있었습니다.


Join

그렇다면 모든 thread가 종료된 후 main 메소드를 종료시키고 싶을 경우는 어떻게 해야 할까요?

public static void main(String[] args) {
    ArrayList<Thread> threads = new ArrayList<Thread>();
    for(int i=0; i<10; i++) {
        Thread t = new Test(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.");
}

ArrayList 객체인 threads를 만들어 thread 생성 시 저장해주었습니다.
main 메소드가 종료되기 전에 threads에 담긴 각각의 thread를 join 메소드를 통해 호출하여 thread가 종료될 때까지 대기하도록 변경하였습니다.

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.

Runnable

Thread를 상속하여 thread 객체를 구현할 수도 있지만, 보통 Runnable 인터페이스를 구현하도록 하는 방법을 많이 사용합니다.


import java.util.ArrayList;

public class Test implements Runnable {
    int seq;
    public Test(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<Thread>();
        for(int i=0; i<10; i++) {
            Thread t = new Thread(new Test(i));		--- (1)
            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.");
    }
}

Runnable은 인터페이스이기 때문에 implements 하도록 변경되었습니다. (Runnable 인터페이스는 run 메소드를 반드시 구현해야합니다.)

  • (1) : Thread의 생성자로 Runnable 인터페이스를 구현한 객체를 넘길 수 있습니다. (인터페이스를 이용했기때문에 더 유연한 프로그램으로 발전했다고 볼 수 있습니다.)

🛴 마무리

우선 예외처리가 생각보다 개념이 안 잡혀 있었고,
쓰레드도 어느정도 알고 있다고 생각했는데 Runnable을 배우다 보니 또 헷갈리네요..
자바는 만만해보이지만 전혀 만만하지 않은 그런 언어인거 같습니다...

자바의 기본 시리즈가 어느정도 마무리되었습니다. 👏👏👏 (짧은 시리즈긴 하지만...)
이 글을 읽어주신 분들이 최대한 읽기 쉽게, 이해하기 쉽게 말씀 드리고 싶었는데 잘 이해가 가셨을지 모르겠네요!
자바를 공부하시는 모든 분들 화이팅입니다 !!


참고

0개의 댓글