private
이 붙은 변수, 메소드는 해당 클래스에서만 접근이 가능합니다.
public class AccessModifier {
private String secret;
private String getSecret() {
return this.secret;
}
}
secret
변수와 getSecret
메소드는 오직 AccessModifier
클래스에서만 접근이 가능하고 다른 클래스에서는 접근이 불가능합니다.
접근제어자를 별도로 설정하지 않는다면 default
접근제어자가 되어 해당 패키지 내에서만 접근이 가능합니다.
protected
가 붙은 변수, 메소드는 동일 패키지 내의 클래스 또는 해당 클래스를 상속받은 외부 패키지의 클래스에서 접근이 가능합니다.
public
접근제어자가 붙은 변수, 메소드는 어떤 클래스에서라도 접근이 가능합니다.
public class HousePark {
static String lastname = "박";
public static void main(String[] args) {
HousePark pey = new HousePark();
HousePark pes = new HousePark();
}
}
위와 같은 코드에서 HousePark
은 어떤 객체가 생성되어도 항상 "박"이라는 동일한 값을 가집니다.
lastname
에 static
키워드를 붙이면 자바는 메모리 할당을 딱 한번만 하게 되어 메모리 사용에 이점을 보입니다.
이말인 즉슨 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
}
}
위와 같이 count
를 static
으로 설정하면 count
값이 공유되어 객체가 생성될 때마다 count
가 올라갑니다.
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
를 이용하여 오늘 날짜를 바로 구할 수 있습니다.
싱글톤은 단 하나의 객체만을 생성하게 강제하는 패턴입니다.
즉, 클래스를 통해 생성할 수 있는 객체는 한 개만 되도록 하는 것입니다.
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를 출력하는 것을 확인할 수 있었습니다.
try {
...
} catch(예외1) {
...
} catch(예외2) {
...
...
}
예외처리를 위해서 try
catch
문의 기본은 위와 같습니다.
프로그램 도중 예외가 발생하면 프로그램이 중지되거나 예외처리를 했을 경우 catch
구문이 실행됩니다.
int c;
try {
c = 4 / 0;
}catch(ArithmeticException e) {
c = -1;
}
숫자를 0으로 나누었을 때 발생하는 예외를 처리하려면 ArithmeticException
를 catch
문에 입력해주어야합니다.
위 코드는 예외가 발생할 경우 c-=1
을 실행합니다.
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()
가 실행됩니다.
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
은 크게 두가지로 구분되는데요.
RuntimeException
: 실행 시 발생하는 예외
UncheckedException
이라고 합니다.Exception
: 컴파일 시 발생하는 예외
Checked 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");
}
}
이번에는 FooleException
이 Exception
을 상속하도록 변경했습니다.
예측 가능한 Checked Exception
이기 때문에 예외처리를 컴파일러가 강제하게 되고, Test
클래스에서는 컴파일 오류가 발생할 것입니다.
(사용자로 하여금 입력을 받는 경우는 컴파일에서 예외를 예측할 수 없습니다.)
따라서 try..catch
문을 통해 FoolException
을 처리해줘야합니다.
위의 예제를 보면 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` 문으로 빠져버리기때문에 두번째 문장이 실행되지 않습니다.
트랜잭션은 하나의 작업 단위를 의미합니다. 예외처리와 매우 밀접한 관련이 있습니다.
상품 발송에 대한 트랜잭션에는 아래와 같습니다.
만약 포장, 영수증 발행, 발송 중 하나라도 실패하면 모두 취소한다고 가정해봅시다. 이럴때 예외처리는 어떻게 할까요?
상품발송() {
try {
포장();
영수증발행();
발송();
} catch (예외) {
모두취소();
}
}
포장() throws 예외 {
...
}
영수증발행() throws 예외 {
...
}
발송() throws 예외 {
...
}
만약, 상품발송()
에 try..catch
문이 있는 것이 아닌 포장()
, 영수증발행()
, 발송()
에 각각 try..catch
문이 있다면 발송은 됐는데 포장이 엉망이라던지 하는 상황이 발생할 수 있습니다.
이렇게 여러 트랜잭션에 유의하면서 적절한 방식을 찾아야합니다.
동작하고 있는 프로그램을 프로세스(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
메소드가 먼저 종료되는 것을 확인할 수도 있었습니다.
그렇다면 모든 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.
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
을 배우다 보니 또 헷갈리네요..
자바는 만만해보이지만 전혀 만만하지 않은 그런 언어인거 같습니다...
자바의 기본 시리즈가 어느정도 마무리되었습니다. 👏👏👏 (짧은 시리즈긴 하지만...)
이 글을 읽어주신 분들이 최대한 읽기 쉽게, 이해하기 쉽게 말씀 드리고 싶었는데 잘 이해가 가셨을지 모르겠네요!
자바를 공부하시는 모든 분들 화이팅입니다 !!