728x90

 

 

자바에는 8대 자료형이라 불리는 기본 자료형이 있다.

이 기본 자료형을 특별하게 분류하는 이유는 클래스가 아니기 때문이다.

클래스가 아니라는 것은 다른 말로 참조타입(reference type)이 아니라는 뜻이다.

 

당장 적당한 예시가 떠오르지 않지만, 이 기본 타입들에 대해 객체로 표현해야 하는 경우가 있는데 문제는 참조타입이 아니기 때문에 객체를 생성하지 못하는 문제가 있다.

 

그래서 Java에는 Wrapper(래퍼) 라고 부르는 클래스가 존재한다.

이 클래스들은 기본 자료형을 참조타입으로 객체를 생성할 수 있도록 해준다.

 

기본 타입 (primitive type) 래퍼클래스 (Wrapper class)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

 

Java에서는 primitive type 과 Wrapper 타입의 변환을 자동으로 해주는데, 이걸을 AutoBoxing 이라고 한다.

다음 코드는 박싱과 언박싱, 그리고 자동 박싱과 자동 언박싱의 예를 보여준다.

 

package com.tistory.xxxelppa.section_001;

public class BoxingUnboxing {
    public static void main(String[] args) {
        Integer myWrapperNumber = new Integer(21);    // Boxing
        int myPrimitiveNumber = myWrapperNumber.intValue();  // Unboxing
        
        Integer myAutoBoxingNumber = 21;                // AutoBoxing
        int myAutoUnBoxingNumber = myAutoBoxingNumber;  // AutoUnBoxing
    }
}

 

꽤나 편리해 보이는데 왜 되도록 피하라고 했을까?

 

 

728x90

 

 

다음 코드를 보자

 

    private static long sum_with_autoboxing() {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            sum += i;
        }
        return sum;
    }

 

그다지 문제가 있어보이지 않는 0부터 4바이트 정수 자료형 int 의 최대 표현 범위의 값의 누적 합을 계산해 long 타입의 변수에 담는 코드이다.

 

조금 더 정확히 얘기하면 long 타입의 i 값을 Long 타입의 변수에 누적 합을 구하고 있다.

문제는 sum += i; 이 부분이다.

Wrapper 타입의 변수에 값을 할당할 때 우리 눈에는 그저 sum += i; 만 보이지만

컴파일러는 이 코드를 sum += Long.valueOf(i); 로 바꾸기 때문이다.

 

즉, 저 라인이 실행 될 때마다 Long 타입의 객체를 생성 한다는 것을 의미한다.

지금의 경우 21억개의 무의미한 객체를 생성했다고 생각하면 될 것 같다.

 

 

그래서 위의 코드에서 딱 한 문자인 L 을 l 로 고쳐보았다.

 

    private static long sum_without_autoboxing() {
        long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            sum += i;
        }
        return sum;
    }

 

그럼 이게 얼마나 차이가 나는걸까?

 

package com.tistory.xxxelppa.section_001;

/**
 * autoboxing 테스트
 *
 * [실행 결과]
 * use autoboxing : 6644 ms
 * not use autoboxing : 609 ms
 *
 * 되도록 박싱된 기본 타입보다 기본 타입을 사용하고
 * 의도하지 않은 오토박싱이 사용되지 않도록 주의 필요
 *
 */
public class Main {
    
    public static void main(String[] args) {
        long startTime, endTime;
        
        startTime = System.currentTimeMillis();
        sum_with_autoboxing();
        endTime = System.currentTimeMillis();
        System.out.println("use autoboxing : " + (endTime - startTime) + " ms");
        
        startTime = System.currentTimeMillis();
        sum_without_autoboxing();
        endTime = System.currentTimeMillis();
        System.out.println("not use autoboxing : " + (endTime - startTime) + " ms");
        
        
    }
    
    private static long sum_with_autoboxing() {
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            sum += i;
        }
        return sum;
    }
    
    private static long sum_without_autoboxing() {
        long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            sum += i;
        }
        return sum;
    }
}

 

코드의 맨 위에 주석으로 실행 결과를 기록해 두었다.

매변 결과는 조금씩 다르겠지만, 6000ms 즉 6초나 더 오래 걸린것을 확인할 수 있었다.

 

 

물론 이렇게 극단적인 상황이 자주 발생하지는 않겠지만, Auto Boxing 을 사용했을 때 내부에서 어떤 일이 생기는지 알고 있으면 좋을것 같아서 정리해 보았다.

 

 

 

 

728x90
728x90

# 자바의 람다식에 대해 학습하세요.


# 학습할 것

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

 

람다식을 사용하기에 앞서 익명 구현 객체라는 것에 대해 알면 좋다.

익명 구현 객체는 인터페이스나 클래스의 객체를 생성해서 사용할 때, 재사용하지 않는 경우 보통 사용한다.

 

예를 들어 보자.

특정 인터페이스를 사용하기 위해 이 인터페이스를 상속 받은 클래스를 구현하는 방법이 있다.

 

package me.xxxelppa.study.week15;
 
public class Exam_001 {
    public static void main(String[] args) {
        Exam_001_Sub exam_001_sub = new Exam_001_Sub();
        exam_001_sub.doSomething();
    }
}
 
interface AnonymousTest_001 {
    void doSomething();
}
 
// 인터페이스를 구현한 클래스 정의
class Exam_001_Sub implements AnonymousTest_001 {
    @Override
    public void doSomething() {
        System.out.println("do something !!");
    }
}

 

do something !!

 

그런데 만약 Exam_001_Sub 클래스가 AnonymousTest 인터페이스의 구현체로만 사용하고, Exam_001_Sub 클래스가 재사용 되지 않는다고 하면 새로운 클래스 파일을 생성해서 관리하는게 부담이 될 수 있다.

 

그래서 다음과 같이 별도의 클래스를 작성하지 않고 인터페이스를 바로 구현하여 사용하는 방법이 있는데, 이것을 익명 구현 객체라고 부른다. (작성하지 않지만 별도의 클래스 파일이 생성 된다. 본문의 아래쪽에서 자세히 다룬다.)

 

package me.xxxelppa.study.week15;
 
public class Exam_002 {
    public static void main(String[] args) {
        AnonymousTest_002 anonymousTest_002 = new AnonymousTest_002() {
            @Override
            public void doSomething() {
                System.out.println("do something !!");
            }
        };
    
        anonymousTest_002.doSomething();
    }
}
 
interface AnonymousTest_002 {
    void doSomething();
}

 

do something !!

 

람다식 얘기에 앞서 굳이 익명 구현 객체에 대해 언급한 이유는 람다식이 이 익명 구현 객체와 생긴게 비슷하기 때문이다.

그래서 간혹 람다식이 간결하게 표현한 익명 구현 객체 처럼 생각 되지만 실제로 생성되는 코드를 보면 전혀 다르다는 것을 알 수 있다.

자세한 내용은 뒤쪽에 정리해 두었다.

 

 

함수형 인터페이스


람다식의 사용 방법에 정리하기에 앞서 함수형 인터페이스에 대해서 알아야 한다.

말이 거창해서 그렇지 결론부터 얘기하면 추상 메소드가 하나뿐인 인터페이스를 함수형 인터페이스라고 한다.

 

즉, 다음과 같은 인터페이스들을 모두 함수형 인터페이스에 속한다.

 

package me.xxxelppa.study.week15;
 
public class Exam_003 {
}
 
interface FunctionalInterface_001 {
    void aaa();
}
 
// 함수형 인터페이스는 @FunctionalInterface 를 사용해서 명시적으로 컴파일러에게 알려줄 수 있다.
// @FunctionalInterface 를 사용하면 다른 사람이 추상 메소드를 추가하는 상황을 예방할 수 있다.
@FunctionalInterface
interface FunctionalInterface_002 {
    void aaa();
    
    // static 메소드가 있어도 괜찮다.
    static void bbb() { }
    
    // default 메소드가 있어도 괜찮다.
    default void ccc() { }
}

 

위에 선언한 두 개의 인터페이스의 특징은, 이 인터페이스를 상속 받은 클래스가 반드시 구현해야하는 추상 메소드가 한 개 뿐이라는 것이다.

 

추가로 오버라이드 한 메소드에 대해 @Override 애노테이션을 붙여주는 것을 기억 할 것이다.

이 애노테이션은 작성하지 않아도 동작하는데 문제가 없지만

붙여 줌으로 컴파일 타임에 Override 된 것이 맞는지 확인하는 것과, 코드를 읽는 입장에서 재정의 된 메소드임을 인지할 수 있다.

 

비슷한 맥락에서 함수형 인터페이스도 @FunctionalInterface 라는 애노테이션을 붙여줄 수 있다.

이 애노테이션을 붙이면 컴파일러가 해당 인터페이스에 추상 메소드가 1개만 선언 되었는지 확인해주며

다른 사람이 이 코드를 보고 추상 메소드를 추가하는 문제를 예방할 수 있다.

 

만약 이 애노테이션이 작성 되어있는데, 복수개의 추상 메소드가 정의되어 있다면 IntelliJ IDE 기준으로 다음와 같은 메시지를 보여준다.

 

"Multiple non-overriding abstract methods found in interface me.xxxelppa.study.week15.FunctionalInterface_002"

 

 

 

 

 

람다식 사용법


람다식은 생략 가능한 부분이 꽤 많기 때문에 헷갈릴 수 있지만, 기본적으로 화살표를 기준으로 좌측에 매개변수가 우측에 실행 코드가 작성 된다는 것을 기억하면 된다.

 

(매개변수) -> { 실행 코드 }

 

위의 형태를 기준으로 String 타입의 문자열을 받아서 그대로 출력하는 경우를 생각해보자.

 

package me.xxxelppa.study.week15;
 
public class Exam_004 {
    public static void main(String[] args) {
        
        // 1. 작성 가능한 모든 내용을 생략 없이 작성한 경우
        LambdaTest_001 myTest_01 = (String param) -> {
            System.out.println(param);
        };
        
        // 2. 매개변수의 타입을 생략한 경우
        LambdaTest_001 myTest_02 = (param) -> {
            System.out.println(param);
        };
        
        // 3. 매개변수가 한개여서 소괄호를 생략한 경우
        LambdaTest_001 myTest_03 = param -> {
            System.out.println(param);
        };
        
        // 4. 실행 코드가 한 줄이어서 중괄호를 생략한 경우
        LambdaTest_001 myTest_04 = (String param) -> System.out.println(param);
        
        // 5. 매개변수가 한개이고 실행 코드가 한 줄이어서 생략 가능한 모든것을 생략한 경우
        LambdaTest_001 myTest_05 = param -> System.out.println(param);
        
        // 6. 반환값이 있는 경우 return 키워드 사용하는 경우
        LambdaTest_002 myTest_06 = (n1, n2) -> {
            System.out.println("6. 반환값이 있는 경우 return 키워드 사용하는 경우");
            return n1 + n2;
        };
    
        // 7. 실행 코드가 반환 코드만 존재하는 경우 키워드와 중괄호 생략한 경우
        LambdaTest_002 myTest_07 = (n1, n2) -> n1 + n2;
        
        // 8. 매개변수가 없어서 소괄호를 생략할 수 없는 경우
        LambdaTest_003 myTest_08 = () -> System.out.println("8. 매개변수가 없어서 소괄호를 생략할 수 없는 경우");
        
        myTest_01.printString("1. 작성 가능한 모든 내용을 생략 없이 작성한 경우");
        myTest_02.printString("2. 매개변수의 타입을 생략한 경우");
        myTest_03.printString("3. 매개변수가 한개여서 소괄호를 생략한 경우");
        myTest_04.printString("4. 실행 코드가 한 줄이어서 중괄호를 생략한 경우");
        myTest_05.printString("5. 매개변수가 한개이고 실행 코드가 한 줄이어서 생략 가능한 모든것을 생략한 경우");
        
        System.out.println();
        System.out.println(myTest_06.add(10, 20));
        System.out.println(myTest_07.add(20, 30));
    
        myTest_08.noArgs();
    }
}
 
@FunctionalInterface
interface LambdaTest_001 {
    void printString(String str);
}
 
@FunctionalInterface
interface LambdaTest_002 {
    int add(int num1, int num2);
}
 
@FunctionalInterface
interface LambdaTest_003 {
    void noArgs();
}

 

1. 작성 가능한 모든 내용을 생략 없이 작성한 경우
2. 매개변수의 타입을 생략한 경우
3. 매개변수가 한개여서 소괄호를 생략한 경우
4. 실행 코드가 한 줄이어서 중괄호를 생략한 경우
5. 매개변수가 한개이고 실행 코드가 한 줄이어서 생략 가능한 모든것을 생략한 경우
6. 반환값이 있는 경우 return 키워드 사용하는 경우
30
50
8. 매개변수가 없어서 소괄호를 생략할 수 없는 경우

 

람다식을 사용할 때 타겟 타입 이라는 것이 존재한다.

타겟 타입이란, 람다식은 기본적으로 '인터페이스 변수'에 담기는데, 이 람다식이 담기는 인터페이스를 타겟 타입이라고 한다.

 

즉, 위에 작성한 예제 코드 기준으로 LambdaTest_001 과 LambdaTest_002 를 타겟 타입이라고 할 수 있다.

 

람다식을 사용할 때 타겟 타입이 중요한 이유는, 위에서 봐서 알겠지만, 람다식만 보고는 이게 어떤 함수형 인터페이스를 구현한 것인지 유추할 수 없기 때문이다.

 

() -> System.out.println("who am I .. ? ");

이것을 보고 void 타입의 매개변수가 없는 추상 메소드를 구현 했다는 것은 알 수 있지만

정확히 어떤 타겟 타입을 사용한 것인지(어떤 함수형 인터페이스를 사용 했는지) 모호하기 때문이다.

 

 

 

추가로 람다식 관련하여 자바 표준 API 가 존재한다.

필요한 경우 직접 함수형 인터페이스를 정의해서 사용해도 무관하지만, 자주 사용하는 형태에 대해 표준으로 정의해서 제공하고 있으니

특별한 경우(?)가 아니라면 직접 인터페이스를 정의해서 사용하는 일은 많지 않을 거라 생각한다.

 

아래는 처음 람다식을 공부했을 당시 정리했던 표이다.

(혹시 오타가 있다면 이 때 당시 전부 타이핑 했었기 때문이니 양해 부탁 드립니다.)

 

종류 추상메소드 특징 인터페이스명 추상메소드 andThen() compose() and()
or()
negate()
설명
Consumer 매개값은 있고,
리턴값은 없음
Consumer<T> void accept(T t) O     객체 T를 받아 소비
BiConsumer<T, U> void accept(T t, U u) O     객체 T와 U를 받아 소비
DoubleConsumer void accept(double value) O     double 값을 받아 소비
IntConsumer void accept(int value) O     int 값을 받아 소비
LongConsumer void accept(long value) O     long 값을 받아 소비
ObjDoubleConsumer void accept(T t, double value)       객체 T와 double 값을 받아 소비
ObjIntConsumer void accept(T t, int value)       객체 T와 int 값을 받아 소비
ObjLongConsumer void accept(T t, long value)       객체 T와 long 값을 받아 소비
Supplier 매개값은 없고,
리턴값은 있음
Supplier<T> T get()       T 객체를 리턴
BooleanSupplier boolean getAsBoolean()       boolean 값을 리턴
DoubleSupplier double getAsDouble()       double 값을 리턴
IntSupplier int getAsInt()       int 값을 리턴
LongSupplier long getAsLong()       long 값을 리턴
Function 매개값도 있고,
리턴값도 있음
주로 매개값을
리턴값으로 매핑
(타입변환)
Function<T, R> R apply(T t) O O   객체 T를 객체 R로 매핑
BiFunction<T, U, R> R apply(T t, U u) O     객체 T와 U를 객체 R로 매핑
DoubleFunction<R> R apply(double value)       double을 객체 R로 매핑
IntFunction<R> R apply(int value)       int를 객체 R로 매핑
IntToDoubleFunction double applyAsDouble(int value)       int를 double로 매핑
IntToLongFunction long applyAsLong(int value)       int를 long으로 매핑
LongToDoubleFunction double applyAsDouble(long value)       long을 double로 매핑
LongToIntFunction int applyAsInt(long value)       long을 int로 매핑
ToDoubleBiFunction<T, U> double applyAsDouble(T t, U u)       객체 T와 U를 double로 매핑
ToDoubleFunction<T> double applyAsDouble(T t)       객체 T를 double로 매핑
ToIntBiFunction<T, U> int applyAsInt(T t, U u)       객체 T와 U를 int로 매핑
ToIntFunction<T> int applyAsInt(T t)       객체 T를 int로 매핑
ToLongBiFunction<T, U> long applyAsLong(T t, U u)       객체 T와 U를 long으로 매핑
ToLongFunction<T> long applyAsLong(T t)       객체 T를 long으로 매핑
Operator 매개값도 있고,
리턴값도 있음
주로 매개값을
연산하고 결과를 리턴
BinaryOperator<T> BiFunction<T, U, R>의 하위 인터페이스 O     T와 U를 연산한 후 R리턴
두 개의 static method
   minBy(Comparator<? Super T> comparator)
   maxBy(Comparator<? Super T> comparator)
UnaryOperator<T> Function<T, T>의 하위 인터페이스       T를 연산한 후 T리턴
DoubleBinaryOperator double applyAsDouble(double, double)       두 개의 double 연산
DoubleUnaryOperator double applyAsDouble(double) O O   한 개의 double 연산
IntBinaryOperator int applyAsInt(int, int)       두 개의 int 연산
IntUnaryOperator int applyAsInt(int) O O   한 개의 int 연산
LongBinaryOperator long applyAsLong(long, long)       두 개의 long 연산
LongUnaryOperator long applyAsLong(long) O O   한 개의 long 연산
Predicate 매개값은 있고,
리턴타입은 boolean
매개값을 조사해서
true/false를 리턴
Predicate<T> boolean test(T t)     O 객체 T를 조사
BiPredicate<T, U> boolean test(T t, U u)     O 객체 T와 U를 비교 조사
DoublePredicate boolean test(double value)     O double 값을 조사
IntPredicate boolean test(int value)     O int 값을 조사
LongPredicate boolean test(long value)     O long 값을 조사

(한빛미디어, 이것이 자바다. 신용권의 Java 프로그래밍 정복 2권 참고)

 

 

각 API 마다 모든 예제 코드를 작성하는 것을 나열하는 것은 별다른 의미가 없을 것 같다.

개인적으로 코드는 작성 하겠지만 여기에는 불필요하게 길어질 것 같아 생략 하려고 한다.

 

메소드, 생성자 레퍼런스에 대해 정리 하면서 Operator 와 Function에 대해 간접적으로 사용 예시를 작성할 예정이다.

 

 

728x90

 

 

 

Variable Capture


람다식의 실행 코드 블록 내에서 클래스의 멤버 필드와 멤버 메소드, 그리고 지역 변수를 사용할 수 있다.

클래스의 멤버 필드와 멤버 메소드는 특별한 제약 없이 사용 가능하지만, 지역변수를 사용함에 있어서는 제약이 존재한다.

이 내용을 잘 이해하기 위해서는 JVM 의 메모리에 대해 조금은 알아야 한다.

 

 

잠시 람다식이 아닌 다른 얘기를 해보자.

멤버 메소드 내부에서 클래스의 객체를 생성해서 사용 할 경우 다음과 같은 문제가 있다.

 

익명 구현 객체를 포함해서 객체를 생성 할 경우 new 라는 키워드를 사용 한다.

이 키워드를 사용한다는 것은 동적 메모리 할당 영역(이하 heap)에 객체를 생성한다는 것을 의미한다.

 

이렇게 생성된 객체는 자신을 감싸고 있는 멤버 메소드의 실행이 끝난 이후에도 heap 영역에 존재하므로 사용할 수 있지만

이 멤버 메소드에 정의 된 매개변수나 지역 변수는 런타임 스택 영역(이하 stack)에 할당되어 메소드 실행이 끝나면 해당 영역에서 사라져 더 이상 사용할 수 없게 된다.

 

그렇기 때문에 멤버 메소드 내부에서 생성된 객체가 자신을 감싸고 있는 메소드의 매개변수나 지역변수를 사용하려 할 때 문제가 생길 수 있다.

 

 

 

조금 더 쉽게 처음부터 설명 하면

 

1. 클래스의 멤버 메소드의 매개변수와 이 메소드 실행 블록 내부의 지역 변수는 JVM의 stack에 생성되고

메소드 실행이 끝나면 stack에서 사라진다.

 

2. new 연산자를 사용해서 생성한 객체는 JVM의 heap 영역에 객체가 생성되고 GC (Garbage Collector)에 의해 관리되며, 더 이상 사용하지 않는 객체에 대해 필요한 경우 메모리에서 제거한다.

 

heap에 생성된 객체가 stack의 변수를 사용하려고 하는데, 사용하려는 시점에 stack에 더 이상 해당 변수가 존재하지 않을 수 있다. 왜냐하면 stack은 메소드 실행이 끝나면 매개변수나 지역변수에 대해 제거하기 때문이다. 그래서 더 이상 존재하지 않는 변수를 사용하려 할 수 있기 때문에 오류가 발생한다.

 

 

 

자바는 이 문제를 Variable Capture 라고 하는 값 복사를 사용해서 해결하고 있다.

 

즉, 컴파일 시점에 멤버 메소드의 매개변수나 지역 변수를 멤버 메소드 내부에서 생성한 객체가 사용 할 경우 객체 내부로 값을 복사해서 사용한다. 하지만 모든 값을 복사해서 사용할 수 있는 것은 아니다.

여기에도 제약이 존재하는데 final 키워드로 작성 되었거나 final 성격을 가져야 한다.

 

final 키워드로 작성 되는 것은 알겠는데, 성격을 가진다는 것은 무엇일까.

final 성격을 가진다는 것은 final 키워드로 선언된 것은 아니지만 ​값이 한 번만 할당 되어​ final 처럼 쓰이는 것을 뜻한다.

 

(java 1.7 까지는 final 을 반드시 명시 했어야 했고, final 을 생략하고 쓸 수 있는 건 java 1.8 부터로 기억한다.)

 

final 키워드 유무에 따라 값이 복사 되는 위치가 달라지는데, 다음과 같이 복사 된다.

 

package me.xxxelppa.study.week15;
 
public class Exam_005 {
    public void testMethod(final String myFinalString_01, String myString_01) {
        final String myFinalString_02 = "myFinalString_02";
        String myString_02 = "myString_02";
        
        class VariableCaptureTest {
            // final 명시하지 않은 경우 멤버 필드로 복사
            // String myString_01
            // String myString_02
            void print() {
                // final 명시한 경우 지역 변수로 복사
                // String myFinalString_01
                // String myFinalString_02
//                System.out.println(myFinalString_01 + " :: " + myFinalString_02);
//                System.out.println(myString_01 + " :: " + myString_02);
            }
        }
        
        new VariableCaptureTest().print();
    }
    
    public static void main(String[] args) {
        Exam_005 exam_005 = new Exam_005();
        exam_005.testMethod("myFinalString_01", "myString_01");
    }
}

09 ~11 라인, 13 ~ 15 라인은 컴파일 시 생성 될 것으로 예상한 내용을 정리한 것이다.

호기심이 생겨서 16, 17 라인을 사용 할 때와 안 할 때 생성되는 바이트 코드를 비교해 보았다.

 

 

당장 눈에 띄는 것은 중첩 클래스 때문에 '외부_클래스'${숫자}'내부_클래스'.class 클래스 파일이 추가로 생성 되었다.

 

비교 결과 값 복사를 볼 수 확인할 수 있었는데, 예상했던 것과 조금 다른 결과가 나왔다.

 

1. final 키워드를 사용한 매개변수 : 값 복사가 일어남

2. final 키워드를 사용한 지역변수 : final 키워드가 사라지고 값 복사가 일어나지 않음

3. final 키워드를 사용하지 않은 매개변수 : final 키워드가 생성되고 값 복사가 일어남

4. final 키워드를 사용하지 않은 지역변수 : final 키워드가 생성되고 값 복사가 일어남

 

결과가 조금 이상한 것 같아서 디컴파일 된 결과가 아닌 바이트 코드를 열어 보았다.

 

// class version 55.0 (55)
// access flags 0x21
public class me/xxxelppa/study/week15/Exam_005 {

  // compiled from: Exam_005.java
  NESTMEMBER me/xxxelppa/study/week15/Exam_005$1VariableCaptureTest
  // access flags 0x0
  INNERCLASS me/xxxelppa/study/week15/Exam_005$1VariableCaptureTest null VariableCaptureTest

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/xxxelppa/study/week15/Exam_005; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // access flags 0x1
  public testMethod(Ljava/lang/String;Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    LDC "myFinalString_02"
    ASTORE 3
   L1
    LINENUMBER 6 L1
    LDC "myString_02"
    ASTORE 4
   L2
    LINENUMBER 21 L2
    NEW me/xxxelppa/study/week15/Exam_005$1VariableCaptureTest
    DUP
    ALOAD 0
    ALOAD 1
    ALOAD 2
    ALOAD 4
    INVOKESPECIAL me/xxxelppa/study/week15/Exam_005$1VariableCaptureTest.<init> (Lme/xxxelppa/study/week15/Exam_005;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
    INVOKEVIRTUAL me/xxxelppa/study/week15/Exam_005$1VariableCaptureTest.print ()V
   L3
    LINENUMBER 22 L3
    RETURN
   L4
    LOCALVARIABLE this Lme/xxxelppa/study/week15/Exam_005; L0 L4 0
    LOCALVARIABLE myFinalString_01 Ljava/lang/String; L0 L4 1
    LOCALVARIABLE myString_01 Ljava/lang/String; L0 L4 2
    LOCALVARIABLE myFinalString_02 Ljava/lang/String; L1 L4 3
    LOCALVARIABLE myString_02 Ljava/lang/String; L2 L4 4
    MAXSTACK = 6
    MAXLOCALS = 5

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 25 L0
    NEW me/xxxelppa/study/week15/Exam_005
    DUP
    INVOKESPECIAL me/xxxelppa/study/week15/Exam_005.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 26 L1
    ALOAD 1
    LDC "myFinalString_01"
    LDC "myString_01"
    INVOKEVIRTUAL me/xxxelppa/study/week15/Exam_005.testMethod (Ljava/lang/String;Ljava/lang/String;)V
   L2
    LINENUMBER 27 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE exam_005 Lme/xxxelppa/study/week15/Exam_005; L1 L3 1
    MAXSTACK = 3
    MAXLOCALS = 2
}

 

자세한 내용은 모르겠지만 찾아보니 ALOAD 명령은 스택 영역의 값을 읽어오는 명령이라고 한다.

 

그리고 눈여겨 봐야 할 부분은 'NEW me/xxxelppa/study/week15/Exam_005$1VariableCaptureTest' 이 부분인데,

중첩 클래스에 대해 새로운 객체를 생성하는 것으로 보인다.

 

이 주제의 마지막 부분에 람다식을 컴파일 한 결과 어떤 결과가 나오는지 확인해볼 예정이니 꼭 기억했으면 좋겠다.

 

 

 

heap 영역에 존재하는 객체가 stack 영역의 변수를 안전하게 사용할 수 있도록 값 복사를 하는 것은 알겠다.

그럼 복사하는 변수는 왜 final 이어야 할까?

final 이라는 것은 값을 변경할 수 없도록 하겠다는 것을 의미하는데, 값이 변경 가능하다면 문제가 생길 수 있기 때문이다.

 

사본을 사용하고 있는데 원본을 외부에서 변경 할 수 있다면, 객체 내부에서 그 값을 마음 놓고 사용할 수 없기 때문이다.

 

 

그러면 자연스럽게, 이런 멤버 메소드의 매개변수나 지역변수가 아닌, 인스턴스 변수에 대해서는 특별한 제약이 없다는 것에 대해 의문이 풀린다.

왜냐하면 그런 인스턴스 변수들은 기본적으로 heap 영역에 존재하기 때문에, 위와 같이 별도로 값을 복사해서 사용 할 필요 없이 직접 heap 영역에 접근해서 사용하면 되기 때문이다.

 

 

그럼 다시 람다 얘기로 돌아와서, 일반적으로 람다식은 클래스의 멤버 메소드 내부에서 사용 된다.

그렇기 때문에 람다식 내부에서 사용하는 외부 변수들에 대해 위에서 얘기한 동일한 문제가 발생하고, 이 문제를 해결하기 위해 Variable Capture 를 한다.

 

궁금하니까 간단한 람다식을 정의하고 컴파일 해보았다.

 

 

package me.xxxelppa.study.week15;
 
public class Exam_011 {
    public static void main(String[] args) {
    
        MyFunctionalInterface mfi_1 = new MyFunctionalInterface() {
            @Override
            public void doProc() {
                System.out.println("익명 구현 객체");
            }
        };
        mfi_1.doProc();
        
        MyFunctionalInterface mfi_2 = () -> System.out.println("람다식");
        mfi_2.doProc();
    }
}
 
@FunctionalInterface
interface MyFunctionalInterface {
    void doProc();
}

 

익명 구현 객체
람다식

 

intelliJ 가 너무 좋은(?)건지 익명 구현 객체에 대한 클래스 파일이 보이지 않아 직접 폴더를 찾아 들어갔다.

 

class Exam_011$1 implements MyFunctionalInterface {
    Exam_011$1() {
    }

    public void doProc() {
        System.out.println("익명 구현 객체");
    }
}

 

익명 구현 객체에 대해서는 클래스 파일이 별도로 생겼는데 람다식에 대한 클래스 파일은 생기지 않았다.

확인해보지 않았지만, 바로 위에서 살펴본 예제에서 중첩 클래스에 대해 NEW 키워드가 있었던 것으로 보아

이 코드도 바이트코드를 확인해보면 같은 내용이 있을 것 같다.

 

 

그럼 람다식은 어떻게 동작하는 걸까. 굉장히 익명 구현 객체를 사용하는 것처럼 보였는데 실제 결과는 매우 달랐기 때문에 궁금하니까 바이트코드를 열어 보았다.

 

이 코드의 바이트 코드를 보면 특이한 걸 볼 수 있다.

 

// class version 55.0 (55)
// access flags 0x21
public class me/xxxelppa/study/week15/Exam_011 {
  // compiled from: Exam_011.java
  NESTMEMBER me/xxxelppa/study/week15/Exam_011$1
  // access flags 0x0
  INNERCLASS me/xxxelppa/study/week15/Exam_011$1 null null
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/xxxelppa/study/week15/Exam_011; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    NEW me/xxxelppa/study/week15/Exam_011$1
    DUP
    INVOKESPECIAL me/xxxelppa/study/week15/Exam_011$1.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 12 L1
    ALOAD 1
    INVOKEINTERFACE me/xxxelppa/study/week15/MyFunctionalInterface.doProc ()V (itf)

   L2
    LINENUMBER 14 L2
    INVOKEDYNAMIC doProc()Lme/xxxelppa/study/week15/MyFunctionalInterface; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      ()V, 
      // handle kind 0x6 : INVOKESTATIC
      me/xxxelppa/study/week15/Exam_011.lambda$main$0()V, 
      ()V
    ]
    ASTORE 2
   L3
    LINENUMBER 15 L3
    ALOAD 2
    INVOKEINTERFACE me/xxxelppa/study/week15/MyFunctionalInterface.doProc ()V (itf)
   L4
    LINENUMBER 16 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE mfi_1 Lme/xxxelppa/study/week15/MyFunctionalInterface; L1 L5 1
    LOCALVARIABLE mfi_2 Lme/xxxelppa/study/week15/MyFunctionalInterface; L3 L5 2
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x100A
  private static synthetic lambda$main$0()V
   L0
    LINENUMBER 14 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "\ub78c\ub2e4\uc2dd"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
}

 

예상 했던 대로 익명 구현 객체에 대해서는 NEW 를 사용해서 객체도 생성 된 것으로 보이고 또 별도의 클래스 파일도 생긴걸 확인 했다.

 

람다식이 쓰인 부분에 대해서는 invokedynamic 이라는 opcode 를 사용했는데,

java 1.8 부터 생긴 것으로 interface 의 default method 와 lambda 식에서 사용 된다고 한다.

 

람다 내부 동작에 대해 참고가 된 글을 링크로 남긴다.

 

람다의 내부동작 #1

람다의 내부동작 #2

 

 

복잡하다면 익명 구현 객체를 사용 할 때와 람다식을 사용했을 때 다음과 같은 차이점이 있다는 것만 기억하면 된다.

1. 람다식은 익명 구현 객체 처럼 별도의 객체를 생성하거나 컴파일 결과 별도의 클래스를 생성하지 않는다는 것

2. 람다식 내부에서 사용하는 변수는 Variable Capture(값 복사)가 발생하며, 이 값은 final이거나 final 처럼 사용해야 한다는 것

 

사실 사용하는 입장에서는 '인스턴스 변수를 제외하고 람다식 내부에서 사용하는 변수는 final이거나 final 성격을 가져야 한다' 고 기억하면 별 문제 없긴 하다.

 

 

 

 

 

메소드, 생성자 레퍼런스


메소드, 생성자 레퍼런스는 람다식을 더 간략하게 표현할 수 있게 해준다..

콜론 두 개 :: 를 사용하며, 크게 다음과 같이 구분할 수 있다.

 

1. static 메소드 참조

    => 클래스_이름::메소드_이름

 

2. 인스턴스 메소드 참조

    => 인스턴스_변수::메소드_이름

 

3. 람다식의 매개변수로 접근 가능한 메소드 참조

    => 매개변수의_타입_클래스_이름::메소드_이름

 

4. 생성자 참조

    => 클래스_이름::new

 

로 사용할 수 있다.

 

각 경우에 대한 간단한 예시를 작성해 보았다.

우선 1, 2, 3번에 해당하는 예제는 다음과 같다.

 

package me.xxxelppa.study.week15;
 
import java.util.function.BiFunction;
import java.util.function.IntBinaryOperator;
import java.util.function.ToIntBiFunction;
 
public class Exam_006 {
    public static void main(String[] args) {
        
        // int 타입 두 개를 받아 int 타입을 반환하는 표준 api 사용
        IntBinaryOperator op;
        
        // static method 참조
        op = (num_01, num_02) -> MyReference.add_static(num_01, num_02);
        System.out.println(op.applyAsInt(10, 20));
    
        op = MyReference::add_static;
        System.out.println(op.applyAsInt(20, 30));
        
        
        // instance method 잠조
        MyReference mr = new MyReference();
        
        op = (num_01, num_02) -> mr.add_instance(num_01, num_02);
        System.out.println(op.applyAsInt(30, 40));
        
        op = mr::add_instance;
        System.out.println(op.applyAsInt(40, 50));
        
        // 람다식의 매개변수로 접근 가능한 메소드 참조
        //
        // 만약 (x, y) -> x.instanceMethod(y) 인 경우가 있는데
        // 이런 경우 사용할 수 있는 방법은 아래와 같다.
        //
        // 아래 코드는 x 문자열에 y 문자열이 포함되어 있는지 결과를 반환하는 예제이다.
        // String 클래스의 contains 를 사용 한다.
        //
        // 이 경우 static method 참조와 형태가 매우 유사해 보이지만
        // x의 타입에 속하는 클래스 다음에 :: 연산자를 사용해서 메소드 참조를 한다.
        BiFunction<String, String, Boolean> myBiFunction;
        myBiFunction = (x, y) -> x.contains(y);
        System.out.println(myBiFunction.apply("java study", "java"));
        
        myBiFunction = String::contains;
        System.out.println(myBiFunction.apply("java online study", "python"));
    }
}
 
class MyReference {
    // static method
    public static int add_static(int num_1, int num_2) {
        return num_1 + num_2;
    }
    
    // instance method
    public int add_instance(int num_1, int num_2) {
        return num_1 + num_2;
    }
}

 

30
50
70
90
true
false

 

다음은 생성자 참조의 예제이다.

 

package me.xxxelppa.study.week15;
 
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
 
public class Exam_007 {
    public static void main(String[] args) {
        
        BiFunction<String, Integer, ConstructorRefTest> bf;
        Function<String, ConstructorRefTest> f;
        Supplier<ConstructorRefTest> s;
        
        bf = (param_1, param_2) -> new ConstructorRefTest(param_1, param_2);
        System.out.println(bf.apply("nimkoes", 17).toString());
    
        System.out.println();
        
        s = ConstructorRefTest::new;
        System.out.println("기본 생성자 : " + s.get().toString());
        
        f = ConstructorRefTest::new;
        System.out.println("String 하나를 받는 생성자 : " + f.apply("nimkoes").toString());
        
        bf = ConstructorRefTest::new;
        System.out.println("String, int 두 개를 받는 생성자 : " + bf.apply("xxxelppa", 71).toString());
        
    }
}
 
class ConstructorRefTest {
    String name;
    int age;
    
    public ConstructorRefTest() {
    }
    
    public ConstructorRefTest(String name) {
        this.name = name;
    }
    
    public ConstructorRefTest(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "ConstructorRefTest{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

ConstructorRefTest{name='nimkoes', age=17}

기본 생성자 : ConstructorRefTest{name='null', age=0}
String 하나를 받는 생성자 : ConstructorRefTest{name='nimkoes', age=0}
String, int 두 개를 받는 생성자 : ConstructorRefTest{name='xxxelppa', age=71}

 

마지막으로 함수형 프로그래밍에 대해서 생각해보려 한다.

 

1급 시민 또는 1급 객체 (First-class citizen)

다음의 세 가지 조건을 모두 만족하는 것들을 말한다.

1. 변수에 할당할 수 있다.

2. 매개변수로 사용할 수 있다.

3. 반환 값으로 사용할 수 있다.

 

당장 생각나는 대표적인 예는 javascript 의 function 이다.

 

function myFunc() {
    console.log("문자열을 출력 합니다.");
}
 
(function myMainFunc(param) {
    // 매개변수로 사용 가능
    console.log("매개변수로 전달 된 함수 실행");
    param();
    console.log("");
    
 
    // 변수에 할당 가능
    let myVal = param;
    console.log("변수에 담긴 함수 실행");
    myVal();
    console.log("");
    
    let returnVal = (function () { return param; })();
    console.log("반환 결과 변수에 담긴 함수 실행");
    returnVal();
    console.log("");
 
})(myFunc);

갑자기 javascript 코드가 나와서 좀 그렇지만

javascript 에서 함수는 변수에 담을 수 있고, 매개변수로도 전달 가능하며, 반환 값으로도 사용할 수 있다.

그렇기 때문에 javascript 에서 함수는 유명한 1급 시민 객체이다.

 

 

다시 자바 얘기를 해보자.

혹시 지금까지 자바 코드를 작성 하면서 메소드를 변수에 담아 전달해본 적이 있었을까?

메소드는 클래스에 종속되어 객체로 전달 하거나 객체를 반환한 적은 있어도, 메소드 자체를 전달해본 적은 없다.

 

그래서 자바의 메소드는 1급 시민 객체가 아니다.

 

하지만 자바의 람다식은 변수에 담을 수 있고, 매개변수로 전달할 수 있으며, 반환 값으로 사용할 수 있다.

 

package me.xxxelppa.study.week15;
 
public class Exam_009 {
    public static void main(String[] args) {
 
        // 변수에 저장
        MyInterface mi = () -> System.out.println("변수에 저장 된 람다식");
        mi.print();
 
        // 매개변수로 전달
        doProc(() -> System.out.println("매개변수로 전달 된 람다식"));
 
        // 반환 값으로 사용
        getProc().print();
    }
 
    public static void doProc(MyInterface mi) {
        mi.print();
    }
 
    public static MyInterface getProc() {
        return () -> System.out.println("반환 값으로 사용 된 람다식");
    }
}
 
interface MyInterface {
    void print();
}

 

변수에 저장 된 람다식
매개변수로 전달 된 람다식
반환 값으로 사용 된 람다식

 

람다식이 세가지 조건을 만족하기 때문에 1급 시민 객체인 것은 알겠는데, 그래서 뭐가 좋다는 걸까

앞서 람다식의 Variable Capture 에 대해 정리 하면서 언급한 내용을 기억해야 한다.

 

heap 영역에 생성된 객체가 stack 영역의 변수를 안정적으로 사용하기 위해 final 또는 final 성격을 가져야 한다.

즉, 변할 수 있는 것을 변하지 않도록 제한을 둔 것이다. 이것을 불변 상태(Immutable)로 만든다고 한다.

 

불변 상태로 만든다는 것에 대해 잘 이해가 되지 않는다면, 조금 더 쉬운 말로 '외부의 상태에 독립적'이라고 표현할 수 있다.

외부의 상태에 독립적이라는 것은 다른 말로 순수 함수라고 할 수 있는데, 다음의 javascript 코드를 보자.

 

let gender = 'male';
 
function myPureFunction (param){
    return "성별 : " + param;
}
 
function myFunction (param) {
    let result = "성별 : ";
    if(param === 'male') {
        result += "남성";
    } else {
        result += "여성";
    }
    return result;
}
 
(function(){
    console.log(myPureFunction("남성"));
    console.log(myFunction(gender));
})();

 

 

실행 결과는 같지만, myPureFunction 과 myFunction 의 차이점은 실행 결과가 외부 변수에 의해 실행 결과에 영향을 주는가 이다.

 

Variable Capture 를 통해 람다식 내부에서 사용하는 지역 변수에 대해 final 이어야 하는 이유도 같은 맥락이라고 할 수 있다.

 

불변 상태로 만들면 지역 변수에 대해 변하지 않는 상수를 사용하기 때문에 동일한 입력에 대해 동일한 결과를 기대할 수 있다.

이것을 부작용(side effect, 부수효과) 이 없다고 한다.

 

동일한 입력에 대해 일관된 결과를 받아볼 수 있다는 것은 다시 말하면

다수의 쓰레드가 동시에 공유 해서 사용한다고 하더라도 일관된 결과를 받아볼 수 있다는 것으로

쓰레드와 관련된 동시성 문제가 생길 원인을 미리 방지할 수 있다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

14주차 : 제네릭  (0) 2021.05.02
12주차 : 애노테이션  (1) 2021.05.02
11주차 : Enum  (0) 2021.05.01
10주차 : 멀티쓰레드 프로그래밍  (0) 2021.05.01
9주차 : 예외 처리  (0) 2021.05.01
728x90

# 자바의 제네릭에 대해 학습하세요.


# 학습할 것

  • 제네릭 사용법
  • 제네릭 메소드 만들기
  • 제네릭 주요 개념(바운디드 타입, 와일드 카드)
  • Erasure

 

제네릭을 사용하는 방법에 대해 정리해보기 전에 왜 필요한지에 대해 알면 언제 사용할지 도움이 될 수 있다.

예전에 관련해서 정리했던 링크를 첨부한다.

 

java 제네릭 (Generic), 내가 알아보기 쉽게 정리 - 1편, 왜 제네릭

 

링크를 추가 했지만, 그래도 간략하게 제네릭이 왜 필요한지 간단하게 정리해보려 한다.

 

 

제네릭을 사용하는 이유에는 흔히 알고있는 컴파일 타임에 타입 체크를 하기 위함이나 타입 캐스팅을 제거하여 프로그램 성능 향상을 위해서 이다.

하지만 보다 궁극적인(?) 목적은 중복코드의 제거에 있다고 생각 한다.

 

예를 들어 다음과 같이 List 에 담긴 내용을 모두 출력하는 메소드를 구현한다고 생각해보자.

list 에 담긴 요소의 타입이 정수인 경우 다음과 같이 만들어볼 수 있다.

 

    public static void printAllIntegers(List list) {
        for(int i = 0; i < list.size(); ++i) {
            System.out.println((int)list.get(i));
        }
    }

 

list 에 담긴 요소의 타입이 문자열인 경우 다음과 같이 만들어볼 수 있다.

 

    public static void printAllStrings(List list) {
        for(int i = 0; i < list.size(); ++i) {
            System.out.println((String)list.get(i));
        }
    }

 

각 메소드의 구현부를 비교해보면 타입 캐스팅 부분을 제외하고 모두 동일하다.

심지어 만약 매개변수로 전달 받은 list 에 담긴 요소에 기대하지 않은 타입의 요소가 담겨 있다면 런타임에 예외가 발생할 것이다.

 

***

첨언이지만, List 클래스의 경우 제네릭을 지원한다. 하지만 위에 작성한 예제는 제네릭을 사용하지 않고 구현 했고 실제로도 동작하는 것을 확인할 수 있다.

이렇게 제네릭을 지원 하지만 제네릭을 사용하지 않고 클래스 자체를 사용하는 경우 raw type (로 타입)을 사용 했다고 한다.

앞으로 정리 하겠지만 로 타입은 되도록 사용하지 말아야 한다.

사용할 경우 앞서 제네릭의 강력한 장점인 컴파일 타임에 타입 체크를 하는 것과 타입 캐스팅을 하지 않음으로 얻는 이득을 얻지 못하기 때문이다.

그럼 왜 자바를 만든 사람들은 제네릭을 만들었으면서 사용을 강제하지 않고 로 타입도 지원하도록 했을까.

그건 아마도 자바 언어 개발자들이 하위 호환성을 유지하기 위해서다.

제네릭은 JDK5 에 추가된 개념인데 사용을 강제하도록 하면 이전 버전에서는 사용할 수 없기 때문이다.

마지막으로 살펴볼 Erasure 도 하위 호환성을 유지하기 위한 내용이다.

***

 

 

위에 작성한 코드를 개선하는 방법에 대해서는 '제네릭 메소드 만들기'에서 자세히 정리하고, 우선 사용 방법에 대해 알아보자.

 

 

 

 

제네릭 사용법


제네릭은 클래스, 인터페이스 그리고 메소드에 사용할 수 있다. 이 때 중요한 것은 매개변수로 '타입'을 전달할 수 있다는 것이다.

이것을 흔히 타입 파라미터 라고 부르고 타입 파라미터의 타입을 작성을 할 때는 각괄호 <> 를 이용한다. 다이아몬드

말로 설명하려니 어려운것 같다. 문자열을 요소로 가지는 리스트를 정의하고 값을 출력하는 예를 살펴보자.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_002 {
    public static void main(String[] args) {
        List without_generic = new ArrayList();
        without_generic.add("제네릭을 사용하지 않은 로타입 리스트");
        
        List<String> with_generic = new ArrayList<>();
        with_generic.add("제네릭을 사용한 리스트");
        
        /*
            객체를 생성할 때 아래와 같이 구체적인 타입을 작성 해야 하지만 생략 가능하다.
            List<String> with_generic = new ArrayList<String>();
            
            이렇게 제네릭을 사용할 때 구체적인 타입 생략이 가능한 것을 다이아몬드 연산자 라고도 한다.
         */
        
        
        // 출력
        String without_generic_String = (String)without_generic.get(0);  // 타입 캐스팅이 필요하다.
        String with_generic_String = with_generic.get(0);                // 타입 캐스팅이 필요하지 않다.
    }
}

 

제네릭을 사용 한다고 하면 위와 같인 각괄호 안에 사용 할 타입을 작성해서 사용하면 된다.

 

이번엔 컴파일 타임에 타입을 체크 한다는 것이 무엇인지 알아보자.

만약 타입 파라미터와 맞지 않는 타입의 값을 사용하려 한다면 다음과 같은 메시지를 보여준다.

 

 

기억해야 하는 것은 '컴파일 타임'에 타입 체크를 한다는 것이다.

 

제네릭을 사용하는 것은 알겠는데, 어떻게 만들어져 있길래 사용할 수 있는 건지에 대해서는 얘기하지 않았다.

직접 제네릭을 사용한 클래스를 만들어 보자.

다음은 타입 매개변수 타입의 값을 하나 저장할 수 있는 클래스를 선언하고 사용한 예제이다.

 

package me.xxxelppa.study.week14;
 
public class Exam_004 {
    
    // T 타입 value 를 저장할 수 있는 클래스
    static class MyGenericClass<T> {
        T value;
        
        MyGenericClass(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    
        public void setValue(T value) {
            this.value = value;
        }
    }
    
    public static void main(String[] args) {
        MyGenericClass<String> mgc_String = new MyGenericClass<>("사과");
        System.out.println(mgc_String.getValue());
    
        mgc_String.setValue("자몽");
        System.out.println(mgc_String.getValue());
    }
}

 

사과
자몽

 

처음 보면 다소 생소할 수 있지만, 차분히 보면 그리 어렵지 않다.

6라인에서 MyGeneric 클래스를 보면 지금까지 정의했던 클래스와 다른점은 <T> 라는 부분이 추가된 것이다.

이 내용을 작성 함으로 인해 MyGeneric 이라는 클래스 내부에서 T 라는 것을 사용할 수 있게 된다.

그리고 이 T 는 이 클래스 내부에서 '타입 매개변수'를 대표하는 값으로 사용 된다.

 

그럼 왜 하필 T 일까?

사실 어떤 문자를 사용해도 상관 없다.

심지어 <myTypeParameter> 라고 해도 잘 동작한다.

물론 이렇게 정의 했을 경우, 너무나 당연하게도 , 위에서 T 라고 쓴 부분을 모두 myTypeParameter 라고 고쳐줘야 한다.

그럼에도 불구하고 T 를 사용한 이유는 흔히 컨벤션 이라고 하는 우리들 사이의 약속이기 때문이다.

되도록 컨벤션을 지켜 코드를 작성하는게 서로 이해하기 쉽기 때문에 특별한 경우가 아니라면 지켜주는게 좋다.

 

만약 타입 파라미터가 두 개 이상일 경우 T 이외에 R, S, E, K, V, N, U 등의 문자를 많이 사용 하고 콤마(,) 로 구분한다.

보통 T(type), R(return type), S(String), E(element), K (key), V(value), N(number) 의 의미로 사용하는 것으로 알고있다.

 

package me.xxxelppa.study.week14;
 
public class Exam_005 {
    static class MyGenericClass<T, M> {
        T value_1;
        M value_2;
        
        public T getValue_1() {
            return value_1;
        }
        
        public void setValue_1(T value_1) {
            this.value_1 = value_1;
        }
    
        public M getValue_2() {
            return value_2;
        }
    
        public void setValue_2(M value_2) {
            this.value_2 = value_2;
        }
    }
    
    public static void main(String[] args) {
        MyGenericClass<String, Integer> mgc = new MyGenericClass<>();
        mgc.setValue_1("사과");
        mgc.setValue_2(1000);
        System.out.println(mgc.getValue_1() + " 한 개 " + mgc.getValue_2() + "원");
    }
}

 

사과 한 개 1000원

 

728x90

 

 

제네릭 메소드 만들기


순서대로 라면 '제네릭 주요 개념' 을 먼저 정리 해야 하지만, 편의상 메소드 만드는 것을 먼저 정리하게 되었다.

앞서 제네릭은 클래스와 인터페이스 그리고 메소드에서 사용할 수 있다고 했는데, 이번엔 메소드에서 사용하는 방법을 알아보려 한다.

 

제네릭 메소드에 대해 정의 하자면, 파라미터와 반환 타입으로 제네릭 타입을 사용하는 것을 말한다.

다음은 타입 파라미터를 하나 전달 받아 그 값을 그대로 반환하는 코드이다.

 

package me.xxxelppa.study.week14;
 
public class Exam_006 {
    
    public static <T> T myGenericTest(T t) {
        return t;
    }
    
    public static void main(String[] args) {
        System.out.println(myGenericTest("자몽"));
        System.out.println(myGenericTest(1500));
    }
}

 

자몽
1500

 

메소드에서 사용할 경우 클래스에서 사용 할때와 다른 부분이 있다.

앞서 클래스에서 사용할 예시에서 getter, setter 에서는 볼 수 없었던 5 라인에 작성된 <T> 가 그렇다.

 

제네릭 클래스에서는 해당 클래스 내부에서 사용 할 타입 파라미터가 무엇인지 알려주기 위해 class 를 선언할 때 알려 주었다면

제네릭 메소드에서는 메소드를 정의할 때, 해당 메소드 내부에서 사용 할 타입 파라미터가 무엇인지 알려주기 위해

메소드를 정의할 때 5라인 처럼 먼저 나열을 해주고 사용해야 한다. 즉, 리턴 타입을 명시하기 전에 작성 되어야 한다.

 

마치 '이제부터 T 라는 문자를 만나면, 그건 타입 파라미터니까 놀라지 마' 라고 알려주는 것과 같다.

 

 

이번엔 처음에 작성했던 List 내용을 모두 출력하던 메소드를 개선해보자.

개선하기 전의 모습은 다음과 같다.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_001 {
    
    public static void printAllIntegers(List list) {
        for(int i = 0; i < list.size(); ++i) {
            System.out.println((int)list.get(i));
        }
    }
    
    public static void printAllStrings(List list) {
        for(int i = 0; i < list.size(); ++i) {
            System.out.println((String)list.get(i));
        }
    }
    
    public static void main(String[] args) {
        List myList_Integers = new ArrayList();
        myList_Integers.add(1);
        myList_Integers.add(2);
        myList_Integers.add(3);
    
        List myList_Strings = new ArrayList();
        myList_Strings.add("가");
        myList_Strings.add("나");
        myList_Strings.add("다");
        
        printAllIntegers(myList_Integers);
        printAllStrings(myList_Strings);
    }
}

 

1
2
3


 

이것을 제네릭 메소드로 구현하면 다음과 같이 작성해볼 수 있다.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_007 {
    
    public static <T> void printAll(List<T> list) {
        for(T t : list) {
            System.out.println(t);
        }
    }
    
    public static void main(String[] args) {
        List<Integer> myList_Integers = new ArrayList<>();
        myList_Integers.add(1);
        myList_Integers.add(2);
        myList_Integers.add(3);
        
        List<String> myList_Strings = new ArrayList<>();
        myList_Strings.add("가");
        myList_Strings.add("나");
        myList_Strings.add("다");
        
        printAll(myList_Integers);
        printAll(myList_Strings);
    }
}

 

실행 해보면 앞서 작성한 것과 동일한 결과가 나오는 것을 확인할 수 있다.

8라인에 작성한 printAll 제네릭 메소드를 보면,

앞서 List 에 담긴 요소의 타입에 따라 서로 다른 메소드를 작성 하고

요소의 값을 사용할 경우 각 타입에 맞게 타입 캐스팅을 해주어야 했는데

 

제네릭을 사용해서 컴파일 타임에 요소의 타입 검사를 통해 런타임 오류를 방지하면서

타입 캐스팅도 하지 않고

보다 범용적인 메소드를 작성할 수 있는 것을 볼 수 있다.

 

 

이쯤이면 제네릭이 굉장히 매력적이라고(?) 생각할 수 있을것 같다.

 

 

이정도만 해도 충분히 훌륭해 보이지만 사실 문제가 남아있다.

물론 내가 생각한건 아니고 수많은 선배 개발자 분들은 이미 알고 있었고 또 해결까지 해두셨으니 걱정할건 없다.

 

 

 

 

제네릭 주요 개념(바운디드 타입, 와일드 카드)


바로 위에서 언급한 남아있는 문제를 해결하기 위한 방법이 바운디드 타입과 와일드 카드이다.

리스트의 모든 요소를 출력하는 예제를 조금 수정해서 다음과 같이 수정해 보았다.

 

다음은 리스트에 담긴 요소를 문자열이라 가정하고, 공백을 기준으로 나누어 그 개수가 몇개인지 출력하는 메소드이다.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_008 {
    
    public static <T> List<Integer> printTokenSizeList(List<T> list) {
        List<Integer> result = new ArrayList<>();
        for(T t : list) {
            result.add(((String)t).split(" ").length);
        }
        return result;
    }
    
    public static void main(String[] args) {
        List<String> myList = new ArrayList<>();
        myList.add("가을 하늘 공활한데 높고 구름 없이");
        myList.add("밝은 달은 우리 가슴 일편단심일세");
        myList.add("무궁화 삼천리 화려 강산");
        myList.add("대한사람 대한으로 길이 보전하세");
        
        List<Integer> result = printTokenSizeList(myList);
        
        for(int elem : result) {
            System.out.println(elem);
        }
    }
}

 

6
5
4
4

 

별다른 문제 없이 의도한 대로 동작하는것 같지만 사실 불편한 부분이 몇가지 있다.

우선 11라인에서 String 타입으로 캐스팅 하는 코드가 있다는 것이다.

 

만약 이 메소드의 매개변수로 다음과 같은 List 가 전달되면 어떻게 될까?

 

        List<Integer> myNewList = new ArrayList<>();
        myNewList.add(1);
        myNewList.add(2);
        myNewList.add(3);

 

컴파일 오류는 발생하지 않지만 런타임 에러가 발생한다.

 

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')

 

제네릭을 사용해서 컴파일 타임에 타입 체크도 해주고, 코드 재사용성도 좋아졌다고 했는데 신통치 않다.

 

이 문제를 해결 하기 위한 개념이 바운디드 타입이다. 이 개념을 사용하면 타입 파라미터의 타입을 제한할 수 있다.

쉽게 얘기하면 타입 파라미터로 사용할 수 있는 타입을 특정 타입으로 제한할 수 있다는 것이다.

 

사용하는 방법은 extends 키워드를 사용해서 다음과 같이 작성해주면 된다.

다음은 위에 작성한 예제에서 타입 파라미터 T의 값을 String 클래스로 제한하도록 수정한 코드이다.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_008 {
    
    public static <T extends String> List<Integer> printTokenSizeList(List<T> list) {
        List<Integer> result = new ArrayList<>();
        for(T t : list) {
            result.add(t.split(" ").length);
        }
        return result;
    }
    
    public static void main(String[] args) {
        List<String> myList = new ArrayList<>();
        myList.add("가을 하늘 공활한데 높고 구름 없이");
        myList.add("밝은 달은 우리 가슴 일편단심일세");
        myList.add("무궁화 삼천리 화려 강산");
        myList.add("대한사람 대한으로 길이 보전하세");
        
        List<Integer> result = printTokenSizeList(myList);
    
        for(int elem : result) {
            System.out.println(elem);
        }
    }
}

 

수정한 부분은 8라인에서 타입 파라미터의 타입을 String으로 제한한 부분과

그렇기 때문에 11라인에서 T 타입의 변수 t 에 대해 String 타입이라는 것을 알고 있기 때문에

더 이상 타입 캐스팅이 의미가 없어 삭제한 부분이다.

 

 

extends 를 사용해서 타입 제한을 하는 경우 제한한 타입을 포함한 해당 타입의 하위 타입들을 사용할 수 있다.

다음 예시는 List에 담긴 모든 요소 값의 합을 반환하는 예제이다.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_010 {
    
    public static <T extends Number> double getSum(List<T> list) {
        double sum = 0.0;
        for(T t : list) sum += t.doubleValue();
        return sum;
    }
    
    public static void main(String[] args) {
        List<Number> myNumber = new ArrayList<>();
        myNumber.add(10);
        myNumber.add(2.5);
    
        System.out.println(getSum(myNumber));
    }
}

 

12.5

 

이렇게 타입을 제한하는 방법중에 와일드 카드를 사용하는 방법이 있다.

와일드 카드는 보통 '모든 것' 을 뜻하는데 * (별표, asterisk) 또는 ? (물음표) 를 사용하는데, 자바 제네릭 에서는 ? 를 사용한다.

 

크게 세가지 형태가 존재한다.

 

1. <?>

    : 모든 종류의 클래스나 인터페이스 타입 사용 가능

 

2. <? extends 상위타입>

    : 상위타입 타입 또는 이 타입의 하위타입만 사용 가능

 

3. <? super 하위타입>

    : 하위타입 타입 또는 이 타입의 상위타입만 사용 하능

 

 

이해를 돕기 위해 다음과 같은 상속 구조를 갖는 클래스들을 정의 했다고 가정하자.

 

 

package me.xxxelppa.study.week14;
 
import java.util.Arrays;
import java.util.List;
 
public class Exam_011 {
    public static void main(String[] args) {
        // List 의 요소 타입으로 제한을 두지 않음
        List<?> wildcard_test = Arrays.asList(
                new Root(),
                new Sub_01(),
                new Sub_02(),
                new Sub_02_Sub(),
                new Exam_011()
        );
        
        // List 의 요소 타입으로 Sub_02 또는 Sub_02 하위 타입으로 제한
        List<? extends Sub_02> wildcard_extends_test = Arrays.asList(
                new Sub_02(),
                new Sub_02_Sub()
        );
        
        // List 의 요소 타입으로 Sub_01 또는 Sub_01 상위 타입으로 제한
        List<? super Sub_01> wildcard_super_test = Arrays.asList(
                new Root(),
                new Sub_01()
        );
    
        wildcard_test.forEach(System.out::println);
        System.out.println();
        wildcard_extends_test.forEach(System.out::println);
        System.out.println();
        wildcard_super_test.forEach(System.out::println);
    }
}
 
class Root {}
class Sub_01 extends Root {}
class Sub_02 extends Root {}
class Sub_02_Sub extends Sub_02 {}

 

me.xxxelppa.study.week14.Root@2f7a2457
me.xxxelppa.study.week14.Sub_01@566776ad
me.xxxelppa.study.week14.Sub_02@6108b2d7
me.xxxelppa.study.week14.Sub_02_Sub@1554909b
me.xxxelppa.study.week14.Sub_02_Sub@1554909b

me.xxxelppa.study.week14.Sub_02@22f71333
me.xxxelppa.study.week14.Sub_02_Sub@13969fbe

me.xxxelppa.study.week14.Root@3498ed
me.xxxelppa.study.week14.Sub_01@1a407d53

 

만약 타입 제한에 위배 되는 경우 다음과 같은 오류 메시지를 보여준다.

 

 

 

 

Erasure


제네릭에 대해 알아보면서 다양한 코드를 접하고 작성 해봤겠지만 특이한? 점이 있다.

바로 타입 파라미터에 primitive 타입을 사용하지 않았다는 것이다.

 

primitive 타입도 타입인데 타입으로 사용하지 못한다는게 이상하다고 생각해야 한다.

결론부터 얘기하면 타입 소거 (type Erasure) 때문이다.

 

이해를 돕기 위해 List<Integer> 를 정의해보자.

 

package me.xxxelppa.study.week14;
 
import java.util.ArrayList;
import java.util.List;
 
public class Exam_012 {
    List<Integer> list = new ArrayList<>();
}

 

이 코드의 바이트 코드를 보면 다음과 같다.

 

// class version 55.0 (55)
// access flags 0x21
public class me/xxxelppa/study/week14/Exam_012 {

  // compiled from: Exam_012.java

  // access flags 0x0
  // signature Ljava/util/List<Ljava/lang/Integer;>;
  // declaration: list extends java.util.List<java.lang.Integer>
  Ljava/util/List; list

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0

    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 7 L1
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    PUTFIELD me/xxxelppa/study/week14/Exam_012.list : Ljava/util/List;
    RETURN
   L2
    LOCALVARIABLE this Lme/xxxelppa/study/week14/Exam_012; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1
}

 

여기서 주목해야 할 부분은 ArrayList 가 생성될 때 타입 정보가 없다는 것이다.

재밌는? 것은 제네릭을 사용하지 않고 raw type 으로 ArrayList를 생성 해도 똑같은 바이트 코드를 볼 수 있다는 것이다.

그리고 내부에서 타입 파라미터를 사용할 경우 Object 타입으로 취급하여 처리 된다.

 

이것을 타입 소거 (type Erasure) 라고 한다.

타입 소거는 제네릭 타입이 특정 타입으로 제한 되어 있을 경우 해당 타입에 맞춰 컴파일시 타입 변경이 발생하고

타입 제한이 없을 경우 Object 타입으로 변경된다.

 

 

 

그럼 왜 이렇게 만들었을까? 그 이유는 하위 호환성을 지키기 위해서이다.

제네릭을 사용하더라도 하위 버전에서도 동일하게 동작해야하기 때문이다.

 

primitive 타입을 사용하지 못하는 것도 바로 이 기본 타입은 Object 클래스를 상속받고 있지 않기 때문이다.

그래서 기본 타입 자료형을 사용하기 위해서는 Wrapper 클래스를 사용해야 한다.

Wrapper 클래스를 사용할 경우 Boxing 과 Unboxing 을 명시적으로 사용할 수도 있지만 암묵적으로도 사용할 수 있으니 구현 자체에는 크게 신경쓸 부분은 없는것 같다.

 

 

 

마지막으로 제네릭과 관련하여 한가지 더 생각해볼 문제가 있다.

 

다음 코드는 제네릭 타입 파라미터를 사용해서 배열을 생성하는 예제이다.

 

package me.xxxelppa.study.week14;
 
import java.util.Arrays;
 
public class Exam_013<T> {
    private T[] myArray;
    
    Exam_013(int size) {
//        myArray = new T[size];  // Type parameter 'T' cannot be instantiated directly
        myArray = (T[]) new Object[size];
    }
    
    public void addElem(int index, T t) {
        myArray[index] = t;
    }
    
    public void printElem() {
        System.out.println(Arrays.toString(myArray));
    }
    
    public static void main(String[] args) {
        Exam_013<String> e2 = new Exam_013<>(3);
        e2.addElem(0, "java");
        e2.addElem(1, "generic");
        
        e2.printElem();
    }
}

 

[java, generic, null]

 

제네릭 타입을 사용해서 배열을 생성하려면 9라인 처럼 쓰는게 편할텐데, 왜 사용하지 못하고 10 라인처럼 생성해야 하는걸까?

 

그 이유는 new 연산자를 사용하기 때문이다.

new 연산자는 동적 메모리 할당 영역인 heap 영역에 생성한 객체를 할당한다.

하지만 제네릭은 컴파일 타임에 동작하는 문법이다.

컴파일 타임에는 T의 타입이 어떤 타입인지 알 수 없기 때문에 Object 타입으로 생성한 다음 타입 캐스팅을 해주어야 사용할 수 있다.

 

연장선에서 static 변수에도 제네릭 타입을 사용할 수 없다.

 

package me.xxxelppa.study.week14;
 
public class Exam_014<T> {
    private T myValue_1;
    // 'me.xxxelppa.study.week14.Exam_014.this' cannot be referenced from a static context
    // private static T myValue_2;
}

 

조금만 생각해보면 그 이유를 금방 알 수 있다.

 

static 키워드를 사용해서 멤버 필드를 선언하게 되면, 특정 객체에 종속되지 않고 클래스 이름으로 접근해서 사용할 수 있다.

제네릭 타입을 사용하면, 위 예제의 경우 Exam_014<String> 과 Exam_014<Integer> 등으로 객체를 생성해서

인스턴스마다 사용하는 타입을 다르게 사용할 수 있어야 하는데

static 으로 선언한 변수가 가능할 수가 없다. 그렇기 때문에 static 변수에는 제네릭 타입을 사용할 수 없다.

 

 

하지만 재미있게도 static 메소드에는 제네릭을 사용할 수 있다.

심지어 위에 작성한 예제에서도 static 메소드를 자유롭게 사용했다.

 

왜 static 변수에는 사용할 수 없었는데 메소드에는 가능했을까.

이것도 조금만 생각해보면 그 이유를 알 수 있다.

 

static 키워드를 사용하면 클래스 이름으로 접근하여 객체를 생성하지 않고 여러 인스턴스에서 공유해서 사용할 수 있다.

변수같은 경우 해당 값을 사용하려면 값의 타입을 알아야 하지만

메소드의 경우 해당 기능을 공유해서 사용하는 것이기 때문에 제네릭 타입 변수 T 를 매개변수로 사용한다고 하면

해당 값은 메소드 안에서 지역 변수로 사용되기 때문에 변수와 달리 메소드는 static 으로 선언 되어 있어도 제네릭을 사용할 수 있다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

15주차 : 람다식  (0) 2021.05.02
12주차 : 애노테이션  (1) 2021.05.02
11주차 : Enum  (0) 2021.05.01
10주차 : 멀티쓰레드 프로그래밍  (0) 2021.05.01
9주차 : 예외 처리  (0) 2021.05.01
728x90

# 자바의 애노테이션에 대해 학습하세요.


# 학습할 것

  • 애노테이션 정의하는 방법
  • @retention
  • @target
  • @documented
  • 애노테이션 프로세서

애노테이션(annotation)을 사전에 찾아보면 '주석'이라고 나온다.

 

위 이미지는 구글 검색 결과인데, 말뭉치 주석 이라는 말이 재밌어서 첨부했다.

 

 

자바에서 주석이라고 하면 크게 세 가지 형태를 떠올릴 수 있다.

1. 단일행 주석

2. 다중행 주석

3. javadoc 주석

 

이 주석들은 보통 작성된 코드를 보는 사람에게 정보를 제공해주기 위해 사용한다.

코드를 언제 누가 작성 했는지, 어떤 메소드가 있으며 어떻게 사용하는지 등 보통 자국어 문자를 사용해서 설명을 작성해 놓은 것이다.

적절한 위치에 작성된 과하지 않은 적절한 주석은 코드를 읽는데 도움을 준다.

 

 

아무튼,

사전에 주석이라고 등록되어있는 이 annotation은 앞서 얘기한 주석들과 성격이 좀 다르다.

사람을 위하기도 하지만 보통 컴파일러를 위해 작성하는 주석이다.

 

애노테이션은 보통 컴파일러에게 문법 오류를 체크할 수 있도록 힌트를 준다거나

또는 빌드시 코드를 자동 생성 한다거나 런타임 시점에 참고해서 특정 동작을 하도록 할 때 보통 사용할 수 있다.

 

대표적인 예로 문법 오류를 체크하는 것과 관련해서 @Override

코드 자동 생성에 대해 롬복(lombok) 라이브러리

런타임 시점에 특정 동작을 하는것에 대해 스프링의 aop 등이 있다.

 

 

애노테이션에서 뿐만 아니라 메타 데이터라는게 있다.

메타 데이터는 데이터를 위한 데이터 라고 하는데, 쉽게 설명하면 그 데이터가 어떤 데이터인지 설명해주는 데이터라고 할 수 있다.

그래서 메타 애노테이션 이라고 하면, 애노테이션을 설명하는 애노테이션 이라고 할 수 있으며

이런 메타 애노테이션은 클래스나 필드에 애노테이션을 작성하듯 애노테이션에 애노테이션을 작성할 수 있다.

 

 

 

애노테이션 정의하는 방법


기본적으로 제공해주는 애노테이션이 아닌 커스텀한 애노테이션을 작성하고 싶다면, 다음과 같이 선언해서 사용할 수 있다.

 

package me.xxxelppa.study.week12;
 
public @interface Exam_001 {
}

 

 

애노테이션도 인터페이스나 enum과 같이 class 파일을 만든다.

 

 

참고로 기본적으로 제공하는 애노테이션의 경우 빌트인 (built-in) 애노테이션 이라고도 하는데, 다음과 같은 것들이 있다.

 

annotation description
@Override 오버라이딩 된 메소드에 사용
@Deprecated 사용을 권장하지 않는 메소드에 사용 (IDE에 취소선으로 표시됨)
@SuppressWarnings 컴파일 경고를 무시하도록 할 때 사용
@SafeVarargs 가변인자 parameter 사용시 경고를 무시할 때 사용 (jdk 1.7 이상)
@FunctionalInterface 함수형 인터페이스임을 명시할 때 사용 (jdk 1.8 이상)

 

(함수형 인터페이스란 추상 메소드가 하나만 존재하는 인터페이스, SAM(Single Abstract Method)이라 하기도 한다.)

 

 

애노테이션은 엘리먼트 (element) 라는 것을 멤버로 가질 수 있다.

다음은 String 타입과 int 타입의 멤버를 가지는 Exam_002 애노테이션을 정의한 것이다.

 

package me.xxxelppa.study.week12;
 
public @interface Exam_002 {
    String name();
    int age();
}

이렇게 선언한 애노테이션은 다음과 같이 적용해서 사용할 수 있다.

 

package me.xxxelppa.study.week12;
 
@Exam_002(name = "nimkoes", age = 23)
public class Exam_003 {
    
}

 

Exam_002 애노테이션의 경우 기본값이 없기 때문에 Exam_003 클래스에 애노테이션을 적용했을 때 그 값을 반드시 지정해줘야 한다.

하지만 다음과 같이 애노테이션에 기본값이 설정 되어 있다면, 애노테이션을 적용할 때 굳이 어떤 값인지 작성하지 않아도 된다.

 

package me.xxxelppa.study.week12;
 
@MyAnnotation_01
public class Exam_004 {
}
 
@interface MyAnnotation_01 {
    String name() default "nimkoes";
    int age() default 23;
}

 

다음으로 애노테이션은 value 라는 기본 element를 가질 수 있다.

value 라는 엘리먼트는 애노테이션을 적용할 때 굳이 element의 이름을 명시해주지 않아도 된다.

 

package me.xxxelppa.study.week12;
 
@MyAnnotation_02("nimkoes")
public class Exam_005 {
}
 
@interface MyAnnotation_02 {
    String value();
}

 

예시로 String 타입을 사용했지만, 굳이 String 타입이 아니어도 괜찮다.

하지만 한 번에 두 개 이상의 엘리먼트 값을 할당해야 하는 경우 value 라고 하더라도 value를 명시해 주어야 한다.

 

package me.xxxelppa.study.week12;
 
@MyAnnotation_03(value = "nimkoes", age = 23)
public class Exam_006 {
}
 
@interface MyAnnotation_03 {
    String value();
    int age();
}

 

여기까지 애노테이션을 작성하는 방법은 살펴보았는데 어쩌면 '그래서 뭐?' 라는 생각이 들지 모르겠다.

 

 

이미 정의 되어있는 애노테이션을 사용하거나, 프레임워크 또는 추가한 라이브러리에서 제공하는 애노테이션을 사용한다면 별로 신경쓰지 않아도 될지 모르겠다.

하지만 커스텀 애노테이션으르 만들거나 또는 애노테이션을 사용해서 커스텀한 무엇인가를 하고 싶다면 자바 리플렉션에 대해 아는것이 필수라고 생각한다.

 

리플렉션은 구체적인 클래스 타입을 몰라도 그 클래스의 메소드, 변수 등에 접근해서 사용할 수 있도록 해주는 것이라고 정의되어 있는데 잘 와닿지 않는다.

아주 단순하고 무식하게 설명하면 .class 라는 저장된 파일을 .java 소스에서 읽어서 그 클래스 파일에 메소드나 변수는 무엇이 있는지 등의 정보에 접근해서 사용할 수 있다고 할 수 있다.

 

이 리플렉션은 Class 라는 클래스로 사용할 수 있다.

 

다음은 리플렉션 기술을 사용해서 내가 정의한 애노테이션이 적용 되어 있는지 확인하는 에제 코드이다.

 

package me.xxxelppa.study.week12;
 
import java.lang.annotation.Annotation;
 
public class Exam_007 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = Class.forName("me.xxxelppa.study.week12.ReflectionTestClass");
        Annotation[] annotations = clazz.getDeclaredAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("적용된 annotation : " + annotation.toString());
        }
    }
}
 
@interface MyAnnotation_04 { }
 
@MyAnnotation_04
class ReflectionTestClass { }

 

(참고로 package 이름을 포함한 클래스 전체 이름을 FQCN (Fully Qualified Class Name) 이라 한다.)

 

위 예제를 실행해보면 놀랍게도 아무것도 출력되지 않는다.

분명히 ReflectionTestClass 에는 MyAnnotation_04 애노테이션이 작성되어있는데 아무것도 출력하지 않는다.

 

그 비밀은 retention 이라고 하는 유지 정책과 관련이 있다.

자연스럽게 다음 주제로 넘어가보자.

 

 

728x90

 

 

 

@retention


애노테이션에는 유지 정책이라는 것이 있다.

말이 거창해서 그렇지 그 애노테이션 정보를 언제까지 유지할 것인지 정하는 것이다.

 

이 유지 정책은 java.lang.annotation.RetentionPolicy 라는 enum 타입으로 정의되어 있다.

1. SOURCE

   : 소스상에서만 애노테이션 정보를 유지한다. 바이트코드에는 정보가 남지 않는다.

2. CLASS

   : 바이트코드까지 애노테이션 정보를 유지한다. 하지만 리플렉션으로 정보를 얻을 수 없다.

3. RUNTIME

   : 리플렉션을 이용해서 런타임에도 정보를 얻을 수 있다.

 

위에 작성한 예제 실행 결과 아무것도 출력하지 않은 것은, Retention을 적용하지 않으면 기본적으로 CLASS 유지 정책이 적용되기 때문이다.

리플렉션을 사용해서 정보를 가져와보려 하는 예제이기 때문에 이 정책을 RUNTIME 으로 바꾼 다음 다시 실행 해보면 다음과 같다.

 

package me.xxxelppa.study.week12;
 
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
public class Exam_007 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = Class.forName("me.xxxelppa.study.week12.ReflectionTestClass");
        Annotation[] annotations = clazz.getDeclaredAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("적용된 annotation : " + annotation.toString());
        }
    }
}
 
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation_04 { }
 
@MyAnnotation_04
class ReflectionTestClass { }

 

적용된 annotation : @me.xxxelppa.study.week12.MyAnnotation_04()

 

 

 

@target


애노테이션과 관련하여 java.lang.annotation.ElementType 이라는 enum 타입이 정의되어 있다.

이 타입은 애노테이션이 어느 곳에 적용할지 그 대상을 제한하는데 사용한다.

 

ElementType 적용 대상
TYPE class, interface, enum
ANNOTATION_TYPE annotation
FIELD field
CONSTRUCTOR constructor
METHOD method
LOCAL_VARIABLE local variable
PACKAGE package

 

적용 대상을 제한할 때는 @Target 을 사용한다.

다음은 메소드에만 정의할 수 있는 애노테이션을 만들어본 예제이다.

 

package me.xxxelppa.study.week12;
 
import java.lang.annotation.*;
import java.lang.reflect.Method;
 
public class Exam_008 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clazz = Class.forName("me.xxxelppa.study.week12.MyTest_008");
 
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            Annotation[] declaredAnnotations = declaredMethod.getDeclaredAnnotations();
            for (Annotation declaredAnnotation : declaredAnnotations) {
                if (declaredAnnotation.annotationType().equals(MyAnnotation_05.class)) {
                    System.out.println(declaredMethod.toString() + " 메소드에 MyAnnotation_05 가 적용되어 있습니다.");
                }
            }
        }
    }
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation_05 {}
 
class MyTest_008 {
    @MyAnnotation_05
    public void print_with_annotation() { }
    public void print_without_annotation() { }
}

 

public void me.xxxelppa.study.week12.MyTest_008.print_with_annotation() 메소드에 MyAnnotation_05 가 적용되어 있습니다.

 

복잡해 보이지만 사실 아주 간단한 예제이다.

만약 Target이 아닌 곳에 애노테이션을 적용하면 "not applicable to type" 이라는 컴파일 오류를 발생한다.

 

마지막으로 @Target 은 값으로 배열을 사용할 수 있다.

이것은 한 번에 여러 적용 대상을 선택할 수 있도록 하기 위함이다.

 

다음은 field 와 method 에 적용 가능한 애노테이션을 정의한 예시이다.

 

package me.xxxelppa.study.week12;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
 
public class Exam_009 {
    
    @MyAnnotation_06
    private String name;
    
    @MyAnnotation_06
    public void method() {
    }
}
 
@Target({ElementType.FIELD, ElementType.METHOD})
@interface MyAnnotation_06 {
}

 

다음은 element 값을 사용하는 예제이다.

 

package me.xxxelppa.study.week12;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
public class Exam_010 {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
        
        Method[] declaredMethods = MyTest_010.class.getDeclaredMethods();
        
        for (Method declaredMethod : declaredMethods) {
            // MyAnnotation_07 annotation 이 있을 경우
            if (declaredMethod.isAnnotationPresent(MyAnnotation_07.class)) {
                
                // MyAnnotation_07 객체
                MyAnnotation_07 myAnnotation_07 = declaredMethod.getAnnotation(MyAnnotation_07.class);
                
                for(int i = 0; i < myAnnotation_07.loopCnt(); ++i) {
                    declaredMethod.invoke(new MyTest_010());
                }
            }
        }
    }
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface MyAnnotation_07 {
    int loopCnt() default 1;
}
 
class MyTest_010 {
    @MyAnnotation_07(loopCnt = 10)
    public void printStarLine() {
        System.out.print("*");
    }
}

 

**********

 

 

이렇게 애노테이션을 활용하면 재미있는 기능들을 만들어낼 수 있고 또 활용하기 나름인것 같다.

 

 

 

 

@documented


이 애노테이션을 사용하면 javadoc 과 같은 도구에 의해 내가 작성한 내용이 문서와 될 수 있도록 한다.

javadoc 은 현재 프로젝트에 대한 api를 html 형식으로 생성해주는 도구하고 생각하면 된다.

 

javadoc 자체는 주제를 벗어나는 것 같아서 관련된 태그만 나열하고, annotation에 @document를 붙였을 때와 붙이지 않았을 때의 차이만 확인해보려 한다.

 

- javadoc 태그 종류

   : @author

   : @deprecated

   : @exception

   : @param

   : @return

   : @see

   : @serial

   : @serialData

   : @serialField

   : @since

   : @throws

   : @version

등이 있다.

 

 

intelliJ 에서 javadoc 을 만드는 방법은 간단하다.

 

Tools 메뉴 하위 Generate JavaDoc 을 선택하면 다음과 같은 팝업을 볼 수 있다.

 

 

Locale : ko_KR

Other command line arguments : -encoding UTF-8 -charset UTF-8 -docencoding UTF-8

 

한글이 있을 경우 위 값을 넣어 주면 깨지지 않는다.

 

 

다음 코드를 포함해서 javadoc 을 만든 결과는 다음과 같다.

 

package me.xxxelppa.study.week12;
 
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
/**
 * java online study week 12 : annotation test class
 *
 * @author nimkoes
 */
public class Exam_011 {
    
    @MyAnnotation_08(name = "nimkoes", age = 23)
    public void print_documented(String printValue) {
        System.out.println(printValue);
    }
    
    @MyAnnotation_09(nickname = "xxxelppa", age = 24)
    public void print(String printValue) {
        System.out.println(printValue);
    }
}
 
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation_08 {
    String name();
    int age();
}
 
@Retention(RetentionPolicy.RUNTIME)
@interface  MyAnnotation_09 {
    String nickname();
    int age();
}

 

 

 

 

javadoc 을 만든 다음 확인해보면 @Document 를 추가한 부분에 대해 내가 작성한 내용이 추가 된 것을 볼 수 있었다.

그 외에도 @Inherited, @Repeatable 등의 커스텀 애노테이션을 만들 때 사용 가능한 메타 애노테이션들도 있다.

 

 

 

 

 

애노테이션 프로세서


자바의 애노테이션 프로세서는 컴파일 타임에 애노테이션 정보를 참고하여 코드를 분석하고 생성하는 등의 작업을 할 수 있는 기능이다.

애노테이션 프로세서의 대표적인 예로 롬복(lombok) 라이브러리가 있다.

 

롬복 라이브러리는 애노테이션 기반으로 컴파일 타임에 바이트 코드를 생성해주는 라이브러리 이다.

 

 

프로젝트에 롬복 라이브러리를 추가 해보자.

 

 

인텔리제이 메뉴의 File > Project Structure 에 들어간다.

 

왼쪽 Libraries 메뉴에 들어가서 + 버튼을 누른 다음 From Maven 을 클릭 한다.

 

org.projectlombok:lombok:1.18.12 버전을 많이 사용하는것 같아서 이 버전을 선택 했다.

라이브러리를 다 받고 나면 다음과 같이 코드를 작성 해보자.

 

package me.xxxelppa.study.week12;
 
public class Exam_012 {
    private String name;
    private int age;
}

 

빌드를 한 다음 바이트코드를 열어보면 다음과 같다.

 

// class version 55.0 (55)
// access flags 0x21
public class me/xxxelppa/study/week12/Exam_012 {

  // compiled from: Exam_012.java

  // access flags 0x2
  private Ljava/lang/String; name

  // access flags 0x2
  private I age

  // access flags 0x1
  public ()V
    L0
      LINENUMBER 3 L0
      ALOAD 0
      INVOKESPECIAL java/lang/Object. ()V
      RETURN
    L1
      LOCALVARIABLE this Lme/xxxelppa/study/week12/Exam_012; L0 L1 0
      MAXSTACK = 1
      MAXLOCALS = 1
}

 

이번엔 롬복 라이브러리가 제공하는 애노테이션 중 @Getter 와 @Setter 를 사용해보자.

 

package me.xxxelppa.study.week12;
 
import lombok.Getter;
import lombok.Setter;
 
@Getter @Setter
public class Exam_012 {
    private String name;
    private int age;
}

 

이전 코드와 비교했을 때 추가한 것은 @Getter 와 @Setter 일 뿐인데
이 내용을 참고해서 컴파일 타임에 바이트코드를 생성한 것을 볼 수 있다.

** 만약 똑같이 했는데 바이트코드가 생기지 않았다면 인텔리제이에서 다음 설정을 활성화 해야 한다.

 

File > Settings 를 클릭한다.

 

annotation processor 를 검색해서 나온 메뉴에 들어가서 오른쪽에 Enable annotation processing 이 활성화 되어 있는지 확인해보자.

 

일단 annotation processor 가 무엇인지 느낌은 왔는데, 어떻게 동작했는지는 아직도 의문이다.

컴파일 타임에 annotation processor 가 있는것을 컴파일러는 어떻게 알고 어노테이션에 대해 전처리를 할 수 있을까?

 

lombok 라이브러리를 조금 더 자세히 들여다보자.

 

라이브러리를 열고 들어가보면 META-INF 하위에 services 라는 폴더가 있는데

그 안에 javax.annotation.processing.Processor 라는 파일이 있다.

이 파일을 열면 다음과 같은 내용이 들어있다.

 

lombok.launch.AnnotationProcessorHider$AnnotationProcessor
lombok.launch.AnnotationProcessorHider$AnnotationProcessor

 

이번엔 lombok.launch.AnnotationProcessorHider 클래스에 중첩클래스인 AnnotationProcessor 를 열어보자.

 

 

 

파일에 작성되어있던 AnnotationProcessorHider 클래스에 중첩 된 두 개의 클래스를 볼 수 있는데,
공통점이 눈에 띈다.
바로 AbstractProcessor 클래스를 상속 받고 있다.


결론부터 얘기하기엔 늦었지만, 어쨌든 결론은, Annotation Processor 를 등록하려면

1. META-INF 디렉토리 하위에 services 디렉토리를 만들고
2. javax.annotation.processing.Processor 라는 파일을 만들고
3. 그 안에 AbstractProcessor 클래스를 상속받아 구현하고 있는 클래스의 FQCN 을 작성 하면 된다.

그러면 이제 컴파일러는 이 정보를 바탕으로 annotation processor 기능을 수행할 수 있게 된다.


더 깊게 찾아본다면, 예상하건데, annotation 을 스캔해서 롬복에서 제공하는 애노테이션이 있으면 (예를 들어 Getter, Setter) 바이트 코드를 컴파일 타임에 자동 생성해주는 부분을 찾을 수 있을것 같다.



마지막으로 이 annotation processor 의 장점이라고 하면, 이러한 동작이 '컴파일 타임'에 이루어지기 때문에
런타임에 비용이 추가되지 않는다는 것이다.

하지만 단점은 (내가 알기로) 공식 지원하는 기능이 아니기 때문에, 어떻게 보면 편법?이나 해킹?이라고 할 수 있다는 것이다.
또 다른 단점은 '알고 쓰면 약인데, 모르고 쓰면 의도하지 않은 동작을 할 수도' 있다는 것이다.


마무리가 이상하지만, 그러니까 잘 알고 쓰자.

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

15주차 : 람다식  (0) 2021.05.02
14주차 : 제네릭  (0) 2021.05.02
11주차 : Enum  (0) 2021.05.01
10주차 : 멀티쓰레드 프로그래밍  (0) 2021.05.01
9주차 : 예외 처리  (0) 2021.05.01
728x90

# 자바의 열거형에 대해 학습하세요.


# 학습할 것

  • enum 정의하는 방법
  • enum이 제공하는 메소드 (values()와 valueOf())
  • java.lang.Enum
  • EnumSet

Enum을 '열거형' 또는 Enumeration 또는 상수집합 이라고도 부른다.

 

상수 목록이 필요해서 class 나 interface 를 활용 하는것을 본 적이 있다.

하지만 class 나 interface 는 그런 용도로 사용하라고 만들어진 것이 아니기 때문에 이런 사용을 지양해야 한다.

 

 

 

 

enum 정의하는 방법


가장 단순한 형태의 enum 클래스는 다음과 같이 정의할 수 있다.

 

package me.xxxelppa.study.week011;
 
public enum WhiteshipLectureList {
    THE_JAVA_JAVA_8,
    THE_JAVA_CODE_MANIPULATION,
    THE_JAVA_APPLICATION_TEST,
    SPRING_FRAMEWORK_INTRODUCTION,
    SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION,
    SPRING_FRAMEWORK_CORE,
    SPRING_FRAMEWORK_WEB_MVC,
    SPRING_BOOT,
    SPRING_BOOT_UPDATED,
    SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT,
    SPRING_SECURITY,
    REST_API,
    SPRING_DATA_JPA,
    INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER;
}

 

class 대신 enum 키워드를 사용해서 만들어주면 된다.

그리고 만약 위와 같이 상수로 사용할 목록만 정의한다면 17라인의 세미콜론은 생략 가능하다.

 

이렇게 정의한 enum 은 보통 switch 문에서 유용하게 사용할 수 있다.

예를 들어 위에 나열한 강의의 수강료를 조회해오는 코드를 다음과 같이 작성해볼 수 있다.

 

package me.xxxelppa.study.week011;
 
public class Exam_001 {
    public static void main(String[] args) {
        
        WhiteshipLectureList list = WhiteshipLectureList.SPRING_FRAMEWORK_CORE;
        int amount = 0;
        
        switch (list) {
            case THE_JAVA_JAVA_8                                  : amount = 55000;  break;
            case THE_JAVA_CODE_MANIPULATION                       : amount = 49500;  break;
            case THE_JAVA_APPLICATION_TEST                        : amount = 66000;  break;
            case SPRING_FRAMEWORK_INTRODUCTION                    : amount = 0;      break;
            case SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION    : amount = 0;      break;
            case SPRING_FRAMEWORK_CORE                            : amount = 55000;  break;
            case SPRING_FRAMEWORK_WEB_MVC                         : amount = 110000; break;
            case SPRING_BOOT                                      : amount = 110000; break;
            case SPRING_BOOT_UPDATED                              : amount = 66000;  break;
            case SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT : amount = 330000; break;
            case SPRING_SECURITY                                  : amount = 88000;  break;
            case REST_API                                         : amount = 99000;  break;
            case SPRING_DATA_JPA                                  : amount = 88000;  break;
            case INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER    : amount = 220000; break;
        }
    
        System.out.println(list + " 수강료는 " + amount + "(원) 입니다.");
    }
}

 

SPRING_FRAMEWORK_CORE 수강료는 55000(원) 입니다.

 

IDE 의 도음을 받아 비교적 쉽게 작성할 수 있었지만, 어딘가 불편하다.

만약 상수를 대표하는 값을 임의로 정한 값으로 사용할 수는 없을까?

 

예제 코드 상황에서는 각 강의의 수강료를 알고 싶기 때문에, 각 상수를 대표하는 값을 수강료를 나타낼 정수 타입의 값으로 만들어 보자.

다음은 수정된 enum 클래스 코드이다.

 

package me.xxxelppa.study.week011;
 
public enum WhiteshipLectureList {
    THE_JAVA_JAVA_8(55000),
    THE_JAVA_CODE_MANIPULATION(49500),
    THE_JAVA_APPLICATION_TEST(66000),
    SPRING_FRAMEWORK_INTRODUCTION(0),
    SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION(0),
    SPRING_FRAMEWORK_CORE(55000),
    SPRING_FRAMEWORK_WEB_MVC(110000),
    SPRING_BOOT(110000),
    SPRING_BOOT_UPDATED(66000),
    SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT(330000),
    SPRING_SECURITY(88000),
    REST_API(99000),
    SPRING_DATA_JPA(88000),
    INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER(220000);
    
    private int amount;
    
    WhiteshipLectureList(int amount) {
        this.amount = amount;
    }
    
    public int getAmount() {
        return this.amount;
    }
}

 

위와 같이 enum 에 정의한 상수 옆에 소괄호 () 를 사용하면

21라인과 같이 생성자를 사용할 수 있다.

 

 

이렇게 생성자를 통해 할당된 값을 사용하려면 25라인처럼 메소드를 정의해주면 된다.

수정한 enum 클래스를 사용해서 수강료를 조회하는 코드를 다시 작성해 보았다.

 

package me.xxxelppa.study.week011;
 
public class Exam_002 {
    public static void main(String[] args) {
        WhiteshipLectureList list = WhiteshipLectureList.SPRING_FRAMEWORK_CORE;
        System.out.println(list + " 수강료는 " + list.getAmount() + "(원) 입니다.");
    }
}

 

SPRING_FRAMEWORK_CORE 수강료는 55000(원) 입니다.

 

결과를 같은데 훨씬 코드가 간결해진 것을 볼 수 있다.

 

 

728x90

 

 

물론 지금 예제에서는 수강료 하나만을 사용했는데, 다음과 같이 원한다면 더 많은 값을 사용할 수 있다.

 

package me.xxxelppa.study.week011;
 
public enum WhiteshipLectureList {
    THE_JAVA_JAVA_8                                  (55000 , "더 자바, Java8"),
    THE_JAVA_CODE_MANIPULATION                       (49500 , "더 자바, 코드를 조작하는 다양한 방법"),
    THE_JAVA_APPLICATION_TEST                        (66000 , "더 자바, 애플리케이션을 테스트하는 다양한 방법"),
    SPRING_FRAMEWORK_INTRODUCTION                    (0     , "스프링 프레임워크 입문"),
    SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION    (0     , "예제로 배우는 스프링 입문(개정판)"),
    SPRING_FRAMEWORK_CORE                            (55000 , "스프링 프레임워크 핵심 기술"),
    SPRING_FRAMEWORK_WEB_MVC                         (110000, "스프링 웹 MVC"),
    SPRING_BOOT                                      (110000, "스프링 부트 개념과 활용"),
    SPRING_BOOT_UPDATED                              (66000 , "스프링 부트 업데이트"),
    SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT (330000, "스프링과 JPA 기반 웹 애플리케이션 개발"),
    SPRING_SECURITY                                  (88000 , "스프링 시큐리티"),
    REST_API                                         (99000 , "스프링 기반 REST API 개발"),
    SPRING_DATA_JPA                                  (88000 , "스프링 데이터 JPA"),
    INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER    (220000, "더 개발자, 인터뷰 가이드");
    
    private int amount;
    private String korDesc;
    
    WhiteshipLectureList(int amount) {
        this.amount = amount;
    }
    
    WhiteshipLectureList(int amount, String korDesc) {
        this.amount = amount;
        this.korDesc = korDesc;
    }
    
    public int getAmount() {
        return this.amount;
    }
    
    public String getKorDesc() {
        return this.korDesc;
    }
}

 

위와 같이 생성자를 오버라이딩해서 사용할 수도 있다.

 

 

 

마지막으로 호기심이 생겨 생성자 안에 문자열을 출력하는 내용을 넣고 코드를 작성 해보았다.

(코드 중복이 심해 코드가 추가된 생성자 부분만 첨부한다.)

    WhiteshipLectureList(int amount, String korDesc) {
        System.out.println("call constructor => " + amount + " :: " + korDesc);
        this.amount = amount;
        this.korDesc = korDesc;
    }

 

수정한 코드를 사용한 예제 코드이다.

 

package me.xxxelppa.study.week011;
 
public class Exam_003 {
    public static void main(String[] args) {
        System.out.println("========================= START =========================");
        System.out.println("===================== enum 변수 선언 ====================");
        WhiteshipLectureList list;
        System.out.println("================== enum 변수에 값 할당 ==================");
        list = WhiteshipLectureList.SPRING_BOOT;
        System.out.println("=================== enum 변수 값 사용 ===================");
        System.out.println(list + " 수강료는 " + list.getAmount() + "(원) 입니다.");
        System.out.println("========================== END ==========================");
    }
}

 

========================= START =========================
===================== enum 변수 선언 ====================
================== enum 변수에 값 할당 ==================
call constructor => 55000 :: 더 자바, Java8
call constructor => 49500 :: 더 자바, 코드를 조작하는 다양한 방법
call constructor => 66000 :: 더 자바, 애플리케이션을 테스트하는 다양한 방법
call constructor => 0 :: 스프링 프레임워크 입문
call constructor => 0 :: 예제로 배우는 스프링 입문(개정판)
call constructor => 55000 :: 스프링 프레임워크 핵심 기술
call constructor => 110000 :: 스프링 웹 MVC
call constructor => 110000 :: 스프링 부트 개념과 활용
call constructor => 66000 :: 스프링 부트 업데이트
call constructor => 330000 :: 스프링과 JPA 기반 웹 애플리케이션 개발
call constructor => 88000 :: 스프링 시큐리티
call constructor => 99000 :: 스프링 기반 REST API 개발
call constructor => 88000 :: 스프링 데이터 JPA
call constructor => 220000 :: 더 개발자, 인터뷰 가이드
=================== enum 변수 값 사용 ===================
SPRING_BOOT 수강료는 110000(원) 입니다.
========================== END ==========================

 

실행 결과를 보면 흥미로웠는데, enum 타입 변수를 실제로 사용하는 순간 모든 상수에 대해 객체가 만들어지는 것을 볼 수 있었다.

 

 

 

마지막으로 enum 클래스는 클래스 타입이지만 new 키워드를 사용해서 객체를 만들 수 없다는 특징이 있다.

실제로 생성자에 private 키워드를 붙여보면 똑똑한 IDE 가 'Modifier 'private' is redundant for enum constructors' 라고 알려준다.

생정자가 private 이라는 것은 외부에서 객체를 생성할 수 없다는 것을 뜻한다.

 

그럼 왜 생성자를 private 으로 강제 했을까.

생각해보면 enum은 상수 목록이다. 상수라는건 변수와 달리 다른 값이 할당 될 수 없고 그런 시도도 하면 안된다.

그렇기 때문에 생성자를 통해서 상수에 다른 값을 할당 하려는 생각조차 하지 못하도록 (동적으로 할당할 수 없도록) 강제하는것 같다.

 

 

 

 

enum이 제공하는 메소드 (values()와 valueOf())


enum 에는 몇가지 기본적으로 제공되는 메소드가 있다.

그 중에 values 는 특이하게..? api 문서에 나와있지 않다.

 

이 values 메소드는 enum 안에 선언된 모든 상수들을 배열로 반환한다.

 

 

package me.xxxelppa.study.week011;
 
public class Exam_004 {
    public static void main(String[] args) {
        WhiteshipLectureList[] lists = WhiteshipLectureList.values();
        for(WhiteshipLectureList list : lists) {
            System.out.println(list.toString() + " (" + list.getAmount() + " 원) => " + list.getKorDesc());
        }
    }
}

 

THE_JAVA_JAVA_8 (55000 원) => 더 자바, Java8
THE_JAVA_CODE_MANIPULATION (49500 원) => 더 자바, 코드를 조작하는 다양한 방법
THE_JAVA_APPLICATION_TEST (66000 원) => 더 자바, 애플리케이션을 테스트하는 다양한 방법
SPRING_FRAMEWORK_INTRODUCTION (0 원) => 스프링 프레임워크 입문
SPRING_FRAMEWORK_INTRODUCTION_REVISED_EDITION (0 원) => 예제로 배우는 스프링 입문(개정판)
SPRING_FRAMEWORK_CORE (55000 원) => 스프링 프레임워크 핵심 기술
SPRING_FRAMEWORK_WEB_MVC (110000 원) => 스프링 웹 MVC
SPRING_BOOT (110000 원) => 스프링 부트 개념과 활용
SPRING_BOOT_UPDATED (66000 원) => 스프링 부트 업데이트
SPRING_AND_JPA_BASED_WEB_APPLICATION_DEVELOPMENT (330000 원) => 스프링과 JPA 기반 웹 애플리케이션 개발
SPRING_SECURITY (88000 원) => 스프링 시큐리티
REST_API (99000 원) => 스프링 기반 REST API 개발
SPRING_DATA_JPA (88000 원) => 스프링 데이터 JPA
INTERVIEW_GUIDE_SOFTWARE_DEVELOPMENT_ENGINEER (220000 원) => 더 개발자, 인터뷰 가이드

 

valueOf 메소드는 enum 안에 존재하는 상수를 가져올 때 사용한다.

실제로 열거된 상수와 대소문자, 공백 까지 모두 완벽히 일치할 경우 해당 상수를 반환하고, 그렇지 않을 경우 예외를 발생 한다.

 

package me.xxxelppa.study.week011;
 
public class Exam_005 {
    public static void main(String[] args) {
        String exists_in_enum = "THE_JAVA_JAVA_8";
        String not_exists_in_enum = "THE_JAVA_JAVA_5";
        
        WhiteshipLectureList myList_1 = WhiteshipLectureList.valueOf(exists_in_enum);
        System.out.println(myList_1.getKorDesc());
        
        try {
            WhiteshipLectureList myList_2 = WhiteshipLectureList.valueOf(not_exists_in_enum);
            System.out.println(myList_2.getKorDesc());
        } catch (IllegalArgumentException e) {
            System.out.println("존재하지 않는 상수를 사용하면 IllegalArgumentException 예외를 발생합니다.");
        }
    }
}

 

더 자바, Java8
존재하지 않는 상수를 사용하면 IllegalArgumentException 예외를 발생합니다.

그 외에도 enum 키워드를 사용한 enum 클래스는 아래에서 설명 할 java.lang.Enum 클래스를 기본적으로 상속받고 있는데,

이 java.lang.Enum 클래스는 또 Object 클래스를 상속 받고 있다.

 

 

그래서 enum 클래스는 기본적으로 Object 클래스에 정의되어 있는 메소드들을 사용할 수 있는데 이 중 일부 메소드는 오버라이드 하지 못하도록 막아두었다.

 

예를 들면 equals(), hashCode(), finalize() 등이 있는데, api에서 보면 final 로 정의 되어 있는것을 볼 수 있다.

 

 

 

 

java.lang.Enum


enum 클래스는 java.lang.Enum 클래스를 상속 받도록 되어 있다.

그렇기 때문에 다중 상속을 지원하지 않는 java에서 enum 클래스는 별도의 상속을 받을 수 없다.

 

뿐만 아니라 api문서를 보면 Enum 클래스의 생성자에 대해 Sole constructor 라고 설명이 작성 되어 있다.

이게 무슨 말인지 잘 몰라서 찾아보니, It is for use by code emitted by the compiler in response to enum type declarations. 라는 뜻인것 같다.

즉, 컴파일러가 사용하는 코드기 때문에 사용자가 호출해서 사용할 수 없다.

 

바로 위에서 java.lang.Enum 클래스도 Object 클래스를 상속 받았다고 했다.

그 중 일부는 final로 정의 되어 있어 오버라이드 할 수 없지만, toString 메소드는 Object로부터 상속 받은 메소드를 재정의 한 것들 중에 유일하게 final로 재정의 하지 않은 메소드이다.

 

 

Enum 클래스에 정의되어 있는 다른 메소드에 대한 예시를 작성해 보았다.

 

package me.xxxelppa.study.week011;
 
public class Exam_006 {
    public static void main(String[] args) {
        WhiteshipLectureList myLec_1 = WhiteshipLectureList.SPRING_FRAMEWORK_CORE;
        WhiteshipLectureList myLec_2 = WhiteshipLectureList.SPRING_DATA_JPA;
    
        System.out.println("myLec_1 ordinal :: " + myLec_1.ordinal());
        System.out.println("myLec_2 ordinal :: " + myLec_2.ordinal());
        System.out.println();
        
        System.out.println("diff ordinal :: " + (myLec_1.ordinal() - myLec_2.ordinal()));
        System.out.println("compareTo :: " + myLec_1.compareTo(myLec_2));
        System.out.println();
        
        System.out.println(myLec_1.name());
        System.out.println(myLec_1.toString());
    }
}

 

myLec_1 ordinal :: 5
myLec_2 ordinal :: 12

diff ordinal :: -7
compareTo :: -7

SPRING_FRAMEWORK_CORE
55000 :: 스프링 프레임워크 핵심 기술

ordinal 은 enum 클래스에 나열된 상수가 몇 번째 나열되어 있는지 zero base 로 넘버링 한 위치를 반환 한다.

compareTo 메소드는 이 ordinal 값의 차이가 얼마나 나는지 반환하는 메소드이다.

 

 

name 은 상수의 값 그 자체를 반환 한다.

toString도 오버라이드 하지 않으면 name과 같은 결과가 나왔겠지만, 지금은 enum 클래스에 toString을 오버라이드 한 결과를 출력하고 있다.

위 예제에서 override 한 메소드는 다음과 같이 정의하고 실행한 결과이다.

    @Override
    public String toString() {
        return this.amount + " :: " + this.getKorDesc();
    }

 

 

 

EnumSet


EnumSet 클래스는 java.util 패키지에 정의 되어 있는 클래스이다.

이름에서 유추할 수 있듯 Set 인터페이스를 구현하고 있으며, 일반적으로 알고있는 Set 자료구조의 특징을 가지고 있다.

예를 들면, 중복을 허용하지 않는다던가 하는 것들이다.

 

EnumSet 클래스는 다음과 같이 사용할 수 있다.

 

package me.xxxelppa.study.week011;
 
import java.util.EnumSet;
 
public class Exam_007 {
    public static void main(String[] args) {
        EnumSet<MyEnum> enumSet = EnumSet.allOf(MyEnum.class);
    
        System.out.println("================= 전체 출력 =================");
        System.out.println(enumSet);
        System.out.println();
        
        EnumSet newEnumSet = EnumSet.of(MyEnum.MON, MyEnum.TUE, MyEnum.WED, MyEnum.THU, MyEnum.FRI);
        
        System.out.println("============= 특정 상수만 출력 ==============");
        System.out.println(newEnumSet);
        System.out.println();
    
        System.out.println("========== 특정 상수 제외하고 출력 ==========");
        System.out.println(EnumSet.complementOf(newEnumSet));
        System.out.println();
        
        System.out.println("================= 범위 출력 =================");
        System.out.println(EnumSet.range(MyEnum.WED, MyEnum.FRI));
        System.out.println();
    }
}
 
enum MyEnum {
    SUN, MON, TUE, WED, THU, FRI, SAT
}

 

================= 전체 출력 =================
[SUN, MON, TUE, WED, THU, FRI, SAT]

============= 특정 상수만 출력 ==============
[MON, TUE, WED, THU, FRI]

========== 특정 상수 제외하고 출력 ==========
[SUN, SAT]

================= 범위 출력 =================
[WED, THU, FRI]

 

이 클래스는 모든 메소드가 static 키워드를 사용하여 정의되어 있기 때문에 객체 생성없이 사용할 수 있다.

객체 생성 없이 사용할 수 있다고 했지만 사실 객체를 생성할 수 없다.

api 문서를 찾아보면 이 클래스는 abstract 키워드를 사용한 추상 클래스이기 때문이다.

 

 

이 클래스에 대해 조사하다보니 비트필드에 대한 얘기가 많았다.

이해는 했는데 어떻게 표현하고 정리해야할지 조심스러워서 잘 정리된 글을 첨부한다.

 

비트 필드 대신 EnumSet을 사용하라

 

 


 

 

EnumSet 이외에 EnumMap 이라는 것도 있다.

 

EnumMap 클래스는 Map 인터페이스를 구현하고 있는데, Map 인터페이스를 구현한 HashMap 또는 TreeMap 등과 비교했을 때

정해진 상수를 사용하기 때문에 해싱을 하지 않고, enum을 정의할 때 이미 순서가 정해져 있기 때문에 성능상 이점이 많다고 한다.

 

EnumMap을 사용하는 간단한 예제를 작성해 보았다.

 

package me.xxxelppa.study.week011;
 
import java.util.EnumMap;
import java.util.Map;
 
public class Exam_008 {
    public static void main(String[] args) {
        EnumMap<WhiteshipLectureList, String> enumMap = new EnumMap<>(WhiteshipLectureList.class);
        
        enumMap.put(WhiteshipLectureList.REST_API, "수강하고싶다.");
        enumMap.put(WhiteshipLectureList.SPRING_FRAMEWORK_CORE, "재미있게 수강 했다.");
    
        for (Map.Entry<WhiteshipLectureList, String> entry : enumMap.entrySet()) {
            System.out.println(entry.getKey().getKorDesc() + " :: " + entry.getValue());
        }
    }
}

 

스프링 프레임워크 핵심 기술 :: 재미있게 수강 했다.
스프링 기반 REST API 개발 :: 수강하고싶다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

14주차 : 제네릭  (0) 2021.05.02
12주차 : 애노테이션  (1) 2021.05.02
10주차 : 멀티쓰레드 프로그래밍  (0) 2021.05.01
9주차 : 예외 처리  (0) 2021.05.01
8주차 : 인터페이스  (0) 2021.05.01
728x90

# 자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.


# 학습할 것

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

멀티쓰레드에 대해 알아보기 전에 미리 알아야 할 것들이 조금 있다.

 

OS(운영체제)에서 실행중인 하나의 프로그램을 프로세스라고 한다. 작업 관리자를 열어보면, 현재 운영체제에서 실행중인 프로세스들을 볼 수 있다.

 

이것들은 OS 로부터 메모리를 할당 받아 동작한다.

 

그리고 멀티 태스킹이라는 것도 있다.

딘어에서 느껴지는 것 그대로 동시에 여러가지 일을 처리하는 것을 말한다.

작업 관리자를 보면 동시에 여러가지 프로그램이 실행되고 있는 것을 볼 수 있는데, 이것도 멀티 태스킹의 하나라고 할 수 있다.

 

그렇다고 해서 멀티 태스킹이라는 것이 꼭 OS 레벨에서 동시에 여러 프로세스를 실행 하는 것만을 뜻하지는 않는다.

하나의 프로세스 안에서도 동시에 여러가지 일을 처리할 수 있다.

예를 들면 카카오톡 pc버전에서 메시지를 보내면서 파일을 전송 한다던가 하는 것들을 동시에 할 수 있다.

 

이렇게 하나의 프로세스 안에서 동시에 여러가지 작업을 할 수 있는 것은 각 작업마다 서로 다른 쓰레드를 생성하기 때문이다.

쓰레드는 작업 흐름이라고 할 수 있는데, 카카오톡의 예시에서 처럼 하나의 프로세스는 여러개의 쓰레드를 생성해서 동시에 처리할 수 있다.

이것을 멀티 쓰레드 라고 한다. 멀티 프로세스와는 서로 다른 것이다.

 

 

이 둘을 구분해야 하는 이유가 있다.

멀티 프로세스에서 각자의 프로세스는 OS로부터 서로 다른 메모리를 할당받아 독립적으로 동작하기 때문에, 하나의 프로세스가 문제가 생겼다고 해도 다른 프로세스에 영향을 주지 않는다.

 

즉, 카카오톡 메신저가 문제가 생겨 비정상 강제 종료를 하게 되었다고 해서 인터넷 브라우저나 IDE가 같이 종료되어 버리지 않는다.

하지만 같은 프로세스 안에서는 얘기가 다르다. 멀티 쓰레드 환경에서 하나의 쓰레드에 문제가 생기면, 그 쓰레드가 속한 프로세스 자체가 죽어버릴 수 있기 때문이다.

 

 

그리고 동시성(concurrency)과 병렬성(parallelism)이 있다.

멀티 쓰레드가 실행 될 때 이 두가지 중 하나로 실행된다.

이것은 cpu의 코어의 수와도 연관이 있는데, 하나의 코어에서 여러 쓰레드가 실행되는 것을 동시성,

멀티 코어를 사용할 때 각 코어별로 개별 쓰레드가 실행 되는 것을 병렬성 이라고 한다.

 

만약 코어의 수가 쓰레드의 수보다 많다면, 병렬성으로 쓰레드를 실행하면 되는데

코어의 수보다 쓰레드의 수가 더 많을 경우 동시성을 고려하지 않을 수 없다.

 

동시성을 고려 한다는 것은, 하나의 코어에서 여러 쓰레드를 실행 할 때 병렬로 실행하는 것처럼 보이지만

사실을 병렬로 처리하지 못하고 한 순간에는 하나의 쓰레드만 처리할 수 있어서 번갈아 가면서 처리하게 되는데

그 번갈아 가면서 수행하는게 워낙 빠르기 때문에 각자 병렬로 실행 되는 것처럼 보일 뿐이다.

 

 

 

Thread 클래스와 Runnable 인터페이스


Thread 클래스와 Runnable 인터페이스 모두 자바에서 쓰레드를 생성할 때 사용하는 것들이다.

다음 예제 코드의 주석을 참고하여 어떻게 쓰레드를 생성할 수 있는지 확인해보려 한다.

 

package me.xxxelppa.study.week10;
 
public class Exam_001 {
    public static void main(String[] args) {
 
        // Runnable 객체를 생성자 매개변수로 념겨줘서 생성하는 방법
        Thread myThread_1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("MT_1 :: Runnable 객체를 Thread 클래스 생성자 매개변수로 사용");
            }
        });
 
        // 위와 같은 방법이지만, 람다를 사용한 방법
        Thread myThread_2 = new Thread(() -> {
            System.out.println("MT_2 :: 람다를 사용하여 Runnable 객체를 Thread 클래스 생성자 매개변수로 사용");
        });
 
        // Thread 클래스를 상속 받은 클래스를 사용한 방법
        Thread myThread_3 = new MyThreadClass();
 
        // 익명 객체를 사용해서 쓰레드를 생성
        Thread myThread_4 = new Thread() {
            @Override
            public void run() {
                System.out.println("MT_4 :: 익명 객체를 사용");
            }
        };
        
        
        
        // 생성한 쓰레드를 실행하기 위해서는 Thread 클래스의 start 메소드를 사용
        myThread_1.start();
        myThread_2.start();
        myThread_3.start();
        myThread_4.start();
 
    }
}
 
class MyThreadClass extends Thread {
    @Override
    public void run() {
        System.out.println("MT_3 :: Thread 클래스를 상속받아서 생성한 쓰레드 :: run 메소드를 override 한다.");
    }
}

 

MT_2 :: 람다를 사용하여 Runnable 객체를 Thread 클래스 생성자 매개변수로 사용
MT_4 :: 익명 객체를 사용
MT_3 :: Thread 클래스를 상속받아서 생성한 쓰레드 :: run 메소드를 override 한다.
MT_1 :: Runnable 객체를 Thread 클래스 생성자 매개변수로 사용

 

실행 결과를 보면 쓰레드를 실행한 순서대로 출력되지 않은 것을 볼 수 있다.

왜냐하면 각자 쓰레드를 시작은 했지만 실제로 자원을 할당 받아 특정한 한 순간에 실행되고 있는 쓰레드는 하나이기 때문에 언제 어떤 쓰레드가 자원을 할당 받아 실행될지는 알 수 없기 때문이다. (이 예제 코드를 기준으로)

 

그러면 별로 차이가 없어 보이는 Thread 클래스를 사용하는 것과 Runnable 인터페이스를 사용하는것에 어떤 차이가 있을까.

사실 이 질문에 이유가 있다. 클래스와 인터페이스의 차이이다.

쓰레드를 만들기 위해 클래스 상속을 받아버리면, 정말로 상속 받아야 할 클래스를 상속받지 못할 수 있기 때문에 보통 Runnable 인터페이스를 구현해서 쓰레드를 만든다.

 

또 다른점은 Thread 클래스가 Runnable 인터페이스 구현 객체를 전달 받아 실행할 수 있기 때문에 Runnable 구현 객체를 재사용할 수 있다.

 

package me.xxxelppa.study.week10;
 
public class Exam_002 {
    public static void main(String[] args) {
 
        new Thread(new MyRunnableClass()).start();
        
    }
}
 
class MyRunnableClass implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable 인터페이스를 구현하여 쓰레드 생성");
    }
}

 

Runnable 인터페이스를 구현하여 쓰레드 생성

 

728x90

 

 

 

쓰레드의 상태


쓰레드에는 몇가지 상태가 있다.

이 상태는 열거 상수로 정의되어 확인해볼 수 있는데, 지금은 상태의 종류에 대해서만 알아보려 한다.

 

1. NEW

  : 쓰레드가 생성 되었지만 아직 start 는 호출하지 않은 상태

2. RUNNABLE

  : 실행 대기중인 상태로 언제든 실행 상태가 될 수 있는 상태

3. WAITING

  : 일시 정지 중 하나로 다른 쓰레드의 notify 를 기다리는 상태

4. TIMED_WAITING

  : 일시 정지 중 하나로 일정 시간 동안 기다리는 상태

5. BLOCKED

  : 일시 정지 중 하나로 공유 객체의 락이 풀리기를 기다리는 상태

    공유 객체의 락이라는 것은, 공유 객체에 대해 이미 다른 쓰레드가 사용중일 때 다른 쓰레드가 사용하지 못하도록 동기화 한 것을 말한다.

6. TERMINETED

  : 모든 실행을 마친 상태

 

다음 예제를 통해 쓰레드의 상태를 출력해보는 것을 확인해보자.

 

package me.xxxelppa.study.week10;
 
public class Exam_007 {
    public static void main(String[] args) {
        ThreadStateChecker threadStateChecker = new ThreadStateChecker(new MyTargetThread());
        threadStateChecker.start();
    }
}
 
class ThreadStateChecker extends Thread {
    Thread thread;
 
    public ThreadStateChecker(Thread thread) {
        this.thread = thread;
    }
 
    @Override
    public void run() {
        while(true) {
            State myState = thread.getState();
            System.out.println("쓰레드의 상태 : " + myState);
 
            if(myState == State.NEW) thread.start();
            if(myState == State.TERMINATED) break;
 
            // 0.5초 통안 쓰레드 일시 정지
            try { Thread.sleep(500); } catch (Exception e) { }
        }
    }
}
 
class MyTargetThread extends Thread {
    @Override
    public void run() {
        for(long i = 0; i < 5000000000l; ++i) { }
 
        // 1.5초 동안 쓰레드 일시 정지
        try { Thread.sleep(1500); } catch (Exception e) { }
 
        for(long i = 0; i < 5000000000l; ++i) { }
    }
}

 

쓰레드의 상태 : NEW
쓰레드의 상태 : RUNNABLE
쓰레드의 상태 : RUNNABLE
쓰레드의 상태 : RUNNABLE
쓰레드의 상태 : TIMED_WAITING
쓰레드의 상태 : TIMED_WAITING
쓰레드의 상태 : TIMED_WAITING
쓰레드의 상태 : RUNNABLE
쓰레드의 상태 : RUNNABLE
쓰레드의 상태 : TERMINATED

 

쓰레드 두 개를 만들어서, 하나는 실행중인 쓰레드의 상태를 체크하는 쓰레드와

상태를 체크 당할 쓰레드를 만들어 상태가 어떻게 바뀌는지 확인해 보았다.

 

상태를 체크 당할 MyTargetThread가 실행 상태임을 확인하기 위해 for문을 공회전 하는 것을 넣었다.

상태를 체크 할 ThreadStateChecker 에서는

아직 쓰레드가 시작 전이라면 start 시키고, 종료 되었다면 break 로 while 문을 종료 하도록 했다.

 

쓰레드는 이렇게 몇가지 상태를 가질 수 있는데, 이 상태 값을 가지고 쓰레드의 동작을 제어할 수 있다.

Thread 클래스에 대한 자세한 정보는 api 문서를 참고하는게 좋을것 같다.

 

 

이 중에 상태 제어와 관련있는 몇가지 메소드들은 다음과 같다.

 

## Thread 클래스에 정의

- void interrupt()

- void join()

- void join(long millis)

- void join(long millis, int nanos)

- void resume()

- static void sleep(long millis)

- static void sleep(long millis, int nanos)

- void stop()

- void suspend()

- static void yield()

 

## Object 클래스에 정의

- notify()

- notifyAll()

- wait()

- wait(long millis)

- wait(long millis, int nanos)

 

java 8 api 문서를 기준으로 목록을 작성해 보았다. 취소선으로 작성된 것들은 안정성 문제로 deprecated 된 것들이다.

 

 

[interrupted]

더보기

이 메소드가 호출 된 쓰레드는 자신이 일시정지 상태가 되었을 때 InterruptedException 을 발생시킨다.

즉, 호출 했다고 바로 예외가 발생하지 않는다.

 

package me.xxxelppa.study.week10;
 
public class Exam_008 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyInterruptedTest();
        thread.start();
 
        // 2초 후에 interrupted 를 호출한다.
        Thread.sleep(2000);
        System.out.println("interrupt 호출");
        thread.interrupt();
    }
}
 
class MyInterruptedTest extends Thread {
    @Override
    public void run() {
        boolean flag = true;
 
        while(flag) {
            long sum = 0;
            try {
                System.out.println("쓰레드가 무엇인가 하는중");
                for(long i = 0; i < 2000000000; ++i) sum += i;
                System.out.println("합계 : " + sum);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                System.out.println("쓰레드 작업 종료");
                flag = false;
            }
        }
    }
}

 

쓰레드가 무엇인가 하는중
합계 : 1999999999000000000
쓰레드가 무엇인가 하는중
interrupt 호출
합계 : 1999999999000000000
쓰레드 작업 종료

 

실행 결과를 보면, 쓰레드가 무엇인가 하는 도중에 interrupt 가 호출 되었지만, sleep 으로 일시 정지 상태가 되었을 때 비로소 예외가 발생한 것을 확인할 수 있다.

 

[yield]

더보기

이 메소드를 호출한 쓰레드는 자신과 레이스 컨디션에 놓여있는 다른 쓰레드 중, 자신과 우선순위가 같거나 높은 쓰레드가 더 많은 실행을 할 수 있도록 양보한다.

package me.xxxelppa.study.week10;
 
public class Exam_009 {
    public static void main(String[] args) {
        MyThread_1 myThread_1 = new MyThread_1();
        MyThread_2 myThread_2 = new MyThread_2();
 
        myThread_1.start();
        myThread_2.start();
 
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
        System.out.println("========== myThread_1 양보 시작 ==========");
        myThread_1.callYield = true;
 
        try { Thread.sleep(1500); } catch (InterruptedException e) { }
        System.out.println("========== myThread_1 양보 종료 ==========");
        myThread_1.callYield = false;
 
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
        myThread_1.isBreak = true;
        myThread_2.isBreak = true;
    }
}
 
class MyThread_1 extends Thread {
    public boolean isBreak = false;
    public boolean callYield = false;
 
    @Override
    public void run() {
        while(!isBreak) {
            if(callYield) {
                Thread.yield();
            } else {
                System.out.println("Thread_1 작업중");
                try { Thread.sleep(300); } catch (InterruptedException e) { }
            }
        }
        System.out.println("Thread_1 작업 종료");
    }
}
 
class MyThread_2 extends Thread {
    public boolean isBreak = false;
 
    @Override
    public void run() {
        while(!isBreak) {
            System.out.println("\t\tThread_2 작업중");
            try { Thread.sleep(300); } catch (InterruptedException e) { }
        }
        System.out.println("Thread_2 작업 종료");
    }
}

 

Thread_1 작업중
Thread_2 작업중
Thread_1 작업중
Thread_2 작업중
Thread_1 작업중
Thread_2 작업중
Thread_2 작업중
Thread_1 작업중
========== myThread_1 양보 시작 ==========
Thread_2 작업중
Thread_2 작업중
Thread_2 작업중
Thread_2 작업중
Thread_2 작업중
========== myThread_1 양보 종료 ==========
Thread_1 작업중
Thread_2 작업중
Thread_1 작업중
Thread_2 작업중
Thread_1 작업중
Thread_2 작업중
Thread_1 작업중
Thread_2 작업 종료
Thread_1 작업 종료

 

구분하기 쉽기 위해 Thread_2 가 작업중인 내용은 tab 간격을 주었다.

실제로 myThread_1 이 양보를 시작한 순간부터는 myThread_2 가 더 많은 실행을 할 수 있도록 양보한 것을 확인할 수 있다. 

 

[join]

더보기

쓰레드를 사용하면 각 작업에 대해 각자 독립적인 작업 흐름을 가지고 처리할 수 있다.

하지만 이렇게 독립적으로 실행을 할 때, 반드시 어떤 작업 보다 먼저 또는 어떤 작업보다 나중에 실행 되어야만 하는 경우가 있다.

예를 들면 방에 들어가려면 문이 열려 있어야만 들어갈 수 있다. 문을 열지도 않고 들어가려고 하면 원치 않는 결과를 만들 수 있다. (문에 머리를 박는다던가 하는..)

 

이런 경우에 사용할 수 있는게 join 메소드 이다.

 

package me.xxxelppa.study.week10;
 
public class Exam_010 {
    public static void main(String[] args) {
        Thread thread = new MyRoom();
        thread.start();
 
        System.out.println("방에 들어가고 싶습니다.");
        
        // main 쓰레드에서 thread 쓰레드가 끝나기를 기다립니다.
        try { thread.join(); } catch (InterruptedException e) { }
 
        System.out.println("문이 열렸으니 방에 들어갑니다.");
 
    }
}
 
class MyRoom extends Thread {
    @Override
    public void run() {
        openDoor();
    }
    
    public void openDoor() {
        System.out.println("\t>> 문을 여는데 2초가 걸립니다.");
        try { Thread.sleep(2000); } catch (InterruptedException e) { }
        System.out.println("\t>> 문이 열렸습니다.");
    }
}

 

방에 들어가고 싶습니다.
>> 문을 여는데 2초가 걸립니다.
>> 문이 열렸습니다.
문이 열렸으니 방에 들어갑니다.

 

같은 join 메소드지만 long 타입의 millis 와 int 타입의 nanos 를 받는 오버로딩 된 메소드들이 있다.

이 값을 넘겨주면, 해당 시간 동안만 종료 되기를 기다리고, 그 시간이 초과하면 기다리지 않고 쓰레드를 계속 진행 한다.

 

 

[wait, notify, notifyAll]

더보기

wait 메소드는 쓰레드를 일시 정지 상태로 만든다. 그리고 notify 메소드는 wait에 의해 일시 정지 된 상태의 쓰레드를 실행 대기 상태로 만든다. 마지막으로 notifyAll 은 wait에 의해 일시 정지 된 모든 쓰레드들을 실행 대기 상태로 만든다.

 

주의할 것은.

이 세가지 메소드는 반드시 동기화 메소드 또는 동기화 블록 내에서만 사용 할 수 있다는 것이다.

 

wait와 notify를 사용해서 서로 번갈아가며 실행하는 쓰레드 예제를 만들어 보았다.

 

package me.xxxelppa.study.week10;
 
public class Exam_011 {
    public static void main(String[] args) {
        MySharedClass mySharedClass = new MySharedClass();
        int workCount = 3;
 
        MyWorker_1 myWorker_1 = new MyWorker_1(mySharedClass, workCount);
        MyWorker_2 myWorker_2 = new MyWorker_2(mySharedClass, workCount);
 
        myWorker_1.start();
        myWorker_2.start();
 
    }
}
 
class MySharedClass {
    public synchronized void doWork_worker_1() {
        System.out.println("worker 1 작업 중");
        notify();
        try { wait(); } catch (InterruptedException e) { }
    }
 
    public synchronized void doWork_worker_2() {
        System.out.println("worker 2 작업 중");
        notify();
        try { wait(); } catch (InterruptedException e) { }
    }
}
 
class MyWorker_1 extends Thread {
    MySharedClass mySharedClass;
    int workCount;
 
    public MyWorker_1(MySharedClass mySharedClass, int workCount) {
        this.workCount = workCount;
        this.mySharedClass = mySharedClass;
    }
 
    @Override
    public void run() {
        for(int i = 0; i < workCount; ++i) {
            mySharedClass.doWork_worker_1();
        }
    }
}
 
class MyWorker_2 extends Thread {
    MySharedClass mySharedClass;
    int workCount;
 
    public MyWorker_2(MySharedClass mySharedClass, int workCount) {
        this.workCount = workCount;
        this.mySharedClass = mySharedClass;
    }
 
    @Override
    public void run() {
        for(int i = 0; i < workCount; ++i) {
            mySharedClass.doWork_worker_2();
        }
    }
}

 

worker 1 작업 중
worker 2 작업 중
worker 1 작업 중
worker 2 작업 중
worker 1 작업 중
worker 2 작업 중

 

 

 

쓰레드 우선순위


앞서 쓰레드가 실행될 때 동시성과 병렬성 이라는 것이 있다고 했다.

쓰레드가 코어의 수보다 많아서 동시성으로 처리 되어야 할 경우 어떤 규칙(?)을 가지고 실행할지 알아야 하는데, 스케줄링 이라고 한다.

이 스케줄링은 두 가지 방식이 있는데, 하나가 우선순위 방식이고 또 다른 방식은 round robin 이라고 한다.

 

우선순위 방식이란 우선순위가 더 높은 쓰레드가 더 많은 실행 시간을 가지도록 하는 것이고, round robin 방식은 자원을 점유하는 시간은 정해져 있는 상태에서 서로 돌아가면서 그 시간동안 실행하는 방식을 말한다.

 

 

이 스레드의 우선순위는 사용자(개발자)가 정할 수 있다.

우선순위는 1 ~ 10까지 설정할 수 있고, 10이 가장 높은 우선순위를 나타낸다. 그리고 아무 우선순위도 할당하지 않으면 기본 값으로 5를 갖는다.

 

package me.xxxelppa.study.week10;
 
public class Exam_003 {
    public static void main(String[] args) {
        Thread myThread_1 = new MyRunnableThread("첫번째 쓰레드");
        Thread myThread_2 = new MyRunnableThread("두번째 쓰레드");
 
        myThread_2.setPriority(Thread.MAX_PRIORITY);
        myThread_1.setPriority(3);
 
        myThread_1.start();
        myThread_2.start();
    }
}
 
class MyRunnableThread extends Thread {
    public MyRunnableThread(String name) {
        setName(name);
    }
 
    @Override
    public void run() {
        int sum = 0;
        for(int i = 0; i < 2147483647; ++i) {sum += i;}
        System.out.println(getName());
    }
}

 

두번째 쓰레드
첫번째 쓰레드

 

하지만 절대적으로 그 우선순위를 따르지는 않는다.

그래도 테스트 해보면 얼추 높은 우선순위를 할당한 쓰레드가 대부분 먼저 끝나는 것을 확인할 수 있다.

 

 

 

 

Main 쓰레드


일반적인 모든 Java 어플리케이션(프로그램)은 Main 이라는 쓰레드를 가지고 있다.

이 Main 쓰레드는 main이라는 메소드로부터 시작할 수 있다.

지금까지 지겹도록(?) 봐온 public static void main(String[] ar) 이게 바로 그 Main 쓰레드를 실행 시킨 메소드 이다.

 

처음 자바를 배울 당시에 '자바 프로그램은 main() 에서 시작해서 main() 에서 끝난다' 라는 말을 들은 적이 있다.

지금 생각해보면 100% 맞는 말은 아닌것 같지만 처음엔 그렇게 생각하는게 마음이 편할것 같다.

 

그렇기 때문에 지금 시점에서 일반적인 자바 프로그램을 만들어서 멀티 쓰레드를 구현하려면

Main 쓰레드를 실행 할 main() 메소드 안에서 새로운 쓰레드를 만들어 줘야 한다.

 

그리고 새롭게 생성한 쓰레드는 Main 쓰레드를 실행 할 main 메소드가 종료될 때 같이 종료 되도록 할 수도 있고,

main 쓰레드가 종료 되는 것과 상관 없이 별도로 생성한 쓰레드가 독자적으로 실행하다 종료되게 할 수도 있다.

 

package me.xxxelppa.study.week10;
 
public class Exam_004 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

 

main

 

현재 실행중인 쓰레드의 이름을 출력 해보았다.

 

 

 


동기화


동기화 관련되어 문제가 발생할 수 있는 경우는, 멀티 쓰레드를 구현 하면서 공유 객체를 사용할 때이다.

여러 쓰레드에서 같은 객체를 공유해서 사용한다고 생각해보자.

 

돈을 넣고 빼는 것을 예로 생각해보자.

만약 동시에 여러 대의 ATM 기기를 점유하고 있다고 생각하자.

현재 계좌에 잔액이 1,000원이 있다.

여러대의 ATM 기계에서 잔액을 조외하면 전부 1,000원이 있다고 할 것이다.

만약 그 순간 다섯 대의 ATM 기계에서 동시에 1,000원을 출금 한다면 어떻게 될까?

5,000원이 나올까?

 

이런 경우를 방지하기 위해 동기화 (synchronized)라는 것을 사용한다. (또는 임계 영역(critical section)이라고도 한다.)

 

동기화를 사용하지 않은 예제 상황을 만들어 보면 다음과 같다.

 

package me.xxxelppa.study.week10;
 
public class Exam_005 {
    public static void main(String[] args) {
        MyAccount myAccount = new MyAccount();
 
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                myAccount.withdrawal(1000);
            }
        };
 
        Thread myThread_1 = new Thread(runnable);
        Thread myThread_2 = new Thread(runnable);
        Thread myThread_3 = new Thread(runnable);
 
        myThread_1.start();
        myThread_2.start();
        myThread_3.start();
    }
}
 
class MyAccount {
    private int myMoney = 1000;
 
    public int withdrawal(int money) {
        System.out.println(">>> 출금 시작");
 
        // 찾으려는 금액보다 잔고가 많은 경우에만 출금
        if(this.myMoney < money) {
            System.out.println(" :: 잔액이 부족합니다.");
            System.out.println(">>> 출금 종료");
            return 0;
        }
 
        int returnMoney = money;
 
        // 인출 하는데 1초가 걸린다고 가정
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        System.out.println(" :: 찾으신 금액 : " + money);
        this.myMoney -= money;
 
        System.out.println(">>> 출금 종료");
        return returnMoney;
    }
}

 

>>> 출금 시작
>>> 출금 시작
>>> 출금 시작
 :: 찾으신 금액 : 1000
>>> 출금 종료
 :: 찾으신 금액 : 1000
>>> 출금 종료
 :: 찾으신 금액 : 1000
>>> 출금 종료

 

통장 잔액이 1,000원 인데 3,000원을 찾는 매직이 벌어질 수 있다. (이렇게 부자가 됩니다.)

찾는 입장에서는 행복할 수 있을지도 모르지만.. 아무튼 이런 일은 생기면 안된다.

 

이런 문제가 생긴 이유는, 공유 객체에 대한 동기화가 되지 않았기 때문이다.

아직 한 사람에 대한 출금 처리가 완료되지 않았는데, 또 다른 사람이 와서 출금을 요청했기 떄문에

또 다른 사람의 입장에서도 잔액이 남아있어서 출금이 가능했다는 문제이다.

 

그래서 동시에 접근했을 때 문제가 될 수 있는 부분에 대해서 동기화 처리를 해주어야 한다.

자바에서는 synchronized 키워드를 사용한다.

이 키워드를 사용하면 한 번에 하나의 쓰레드만 해당 자원에 접근해서 사용할 수 있도록 제한할 수 있다.

 

package me.xxxelppa.study.week10;
 
public class Exam_005 {
    public static void main(String[] args) {
        MyAccount myAccount = new MyAccount();
 
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                myAccount.withdrawal(1000);
            }
        };
 
        Thread myThread_1 = new Thread(runnable);
        Thread myThread_2 = new Thread(runnable);
        Thread myThread_3 = new Thread(runnable);
 
        myThread_1.start();
        myThread_2.start();
        myThread_3.start();
    }
}
 
class MyAccount {
    private int myMoney = 1000;
 
    public synchronized int withdrawal(int money) {
        System.out.println(">>> 출금 시작");
 
        // 찾으려는 금액보다 잔고가 많은 경우에만 출금
        if(this.myMoney < money) {
            System.out.println(" :: 잔액이 부족합니다.");
            System.out.println(">>> 출금 종료");
            return 0;
        }
 
        int returnMoney = money;
 
        // 인출 하는데 1초가 걸린다고 가정
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        System.out.println(" :: 찾으신 금액 : " + money);
        this.myMoney -= money;
 
        System.out.println(">>> 출금 종료");
        return returnMoney;
    }
}

 

>>> 출금 시작
 :: 찾으신 금액 : 1000
>>> 출금 종료
>>> 출금 시작
 :: 잔액이 부족합니다.
>>> 출금 종료
>>> 출금 시작
 :: 잔액이 부족합니다.
>>> 출금 종료

 

앞선 예제와 비교해서 수정한 것은 27라인에 synchronized 하나뿐인데 의도한 대로 잘 동작하는것을 볼 수 있다.

 

 

마지막으로 synchronized 블록이 있다.

위에서 처럼 메소드에도 이 키워드를 붙여서 사용할 수 있지만, 이 키워드 자체로 블록을 만들어 사용할 수 있다.

 

이 블록을 사용하면, 메소드 전체에 대해 동기화(또는 락)을 걸어버리는 대신 메소드 내에서 특정 부분 특정 객체에 대해 락을 걸어 사용할 수 있다.

 

package me.xxxelppa.study.week10;
 
public class Exam_005 {
    public static void main(String[] args) {
        MyAccount myAccount = new MyAccount();
 
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                myAccount.withdrawal(1000);
            }
        };
 
        Thread myThread_1 = new Thread(runnable);
        Thread myThread_2 = new Thread(runnable);
        Thread myThread_3 = new Thread(runnable);
 
        myThread_1.start();
        myThread_2.start();
        myThread_3.start();
    }
}
 
class MyAccount {
    private int myMoney = 1000;
 
    public int withdrawal(int money) {
        System.out.println(">>> 출금 시작");
 
        int returnMoney = money;
 
        synchronized(this) { // 동기화 하려는 공유 객체를 넣어준다. this를 넣으면 자기 자신이 공유 객체임을 뜻한다.
            // 찾으려는 금액보다 잔고가 많은 경우에만 출금
            if(this.myMoney < money) {
                System.out.println(" :: 잔액이 부족합니다.");
                System.out.println(">>> 출금 종료");
                return 0;
            }
 
            // 인출 하는데 1초가 걸린다고 가정
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(" :: 찾으신 금액 : " + money);
            this.myMoney -= money;
        }
 
 
        System.out.println(">>> 출금 종료");
        return returnMoney;
    }
}

 

>>> 출금 시작
>>> 출금 시작
>>> 출금 시작
 :: 찾으신 금액 : 1000
>>> 출금 종료
 :: 잔액이 부족합니다.
>>> 출금 종료
 :: 잔액이 부족합니다.
>>> 출금 종료

 

동기화 메소드를 사용했을 때와 비교해보면, 동기화 블록은 해당 영역에 대해서만 동기화를 하기 때문에

해당 임계 영역을 만나기 전까지 동시 호출은 가능한 것을 볼 수 있다.

 

 

 

 

데드락


데드락은 다른 말료 교착상태 라고도 한다.

교착 상태란 쉽게 말해서 오도가도 못하는 난처한 상태이다.

공유 객체에 대해 복수의 쓰레드가 서로 다른 쓰레드의 실행이 끝나기를 기다리고 있는 상태를 말한다.

 

데드락을 볼 수 있는 예제가 있어서 만들어 보았다.

 

package me.xxxelppa.study.week10;
 
public class Exam_006 {
    public static Object myLockObj_1 = new Object();
    public static Object myLockObj2 = new Object();
 
    public static void main(String[] args) {
        MyThread_1 myThread_1 = new MyThread_1();
        MyThread_2 myThread_2 = new MyThread_2();
 
        myThread_1.start();
        myThread_2.start();
    }
    static class MyThread_1 extends Thread {
        @Override
        public void run() {
            synchronized (myLockObj_1) {
                System.out.println("Thread 1 : Holding [myLockObj_1]");
                try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace();}
                System.out.println("Thread 1 : Waiting for [myLockObj_2]");
                synchronized (myLockObj2) {
                    System.out.println("Thread 1 : Holding [myLockObj_1] & [myLockObj_2]");
                }
            }
        }
    }
 
    static class MyThread_2 extends Thread {
        @Override
        public void run() {
            synchronized (myLockObj2) {
                System.out.println("Thread 2 : Holding [myLockObj_2]");
                try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("Thread 2 : Waiting for [myLockObj_1]");
                synchronized (myLockObj_1) {
                    System.out.println("Thread 2 : Holding [myLockObj_1] & [myLockObj_2]");
                }
            }
        }
    }
}

 

 

코드 내용은 단순하다.

Thread 1 은 시작 하자마자 myLockObj_1 객체에 락을 걸고 10ms 이후 myLockIObj_2 에 락을 건다.

Thread 2 는 시작 하자마자 myLockObj_2 객체에 락을 걸고 10ms 이후 myLockIObj_1 에 락을 건다.

 

대략 10ms 이후 서로 상대방이 락을 건 객체를 사용하려 하기 때문에

서로 끝나기를 무한정 기다리는 상황이 만들어졌다.

 

데드락이 걸리는 경우에는 어느 한쪽을 강제로 종료하는 방법밖에 없다고 한다.

 

 

교착상태(deadlock)와 그 해결방안을 알아보자.

 

Deadlock에 빠지지 않는 다양한 방법

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

12주차 : 애노테이션  (1) 2021.05.02
11주차 : Enum  (0) 2021.05.01
9주차 : 예외 처리  (0) 2021.05.01
8주차 : 인터페이스  (0) 2021.05.01
7주차 : 패키지  (0) 2021.05.01
728x90

# 자바의 예외 처리에 대해 학습하세요.


# 학습할 것

  • 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
  • 자바가 제공하는 예외 계층 구조
  • Exception과 Error의 차이는?
  • RuntimeException과 RE가 아닌 것의 차이는?
  • 커스텀한 예외 만드는 방법

 

자바에서 예외 처리 방법 (try, catch, throw, throws, finally), 커스텀한 예외 만드는 방법


자바에서 예외를 처리하는 방법은 다양하다.

 

try ~ catch 를 사용해서 예외를 직접 처리하는 방법.

try ~ catch ~ finally 를 사용해서 예외를 직접 처리하고, 예외가 발생해도 꼭 해야하는 후속처리까지 하는 방법.

throw를 사용해서 강제로 예외를 발생하는 방법.

에라 모르겠다. throws를 사용해서 나를 호출한 녀석에게 예외를 던지는 방법 등이 있다.

 

package me.xxxelppa.study.week09;
 
public class Exam_001 {
    public static void main(String[] args) {
        
        try {
            
            // 예외가 발생할 수 있는 코드를 try 블록 안에 작성
            
        } catch(Exception e) {
            
            // 발생한 예외를 처리할 수 있는 코드를 catch 블록 안에 작성
            // 예외가 발생하지 않으면 이 블록의 내용을 실행하지 않는다.
            
        }
    }
}

 

사실 처음 예외를 보면 finally 가 그다지 쓸모 없어 보이기도 한다.

finally 를 사용하는 다양한 경우가 있겠지만, 데이터베이스를 사용할 때 많이 사용한다.

 

하지만 데이터베이스에 대해 설명하려면 너무 복잡해지기 때문에 상황을 예로 들어보자.

 

냉장고 문을 열었으면 문을 다시 닫고, 음식을 먹었으면 설거지를 해놓는게 보통이다.

 

package me.xxxelppa.study.week09;
 
public class Exam_002 {
    public static void main(String[] args) {
        
        boolean isSnack = true;
        boolean isMilk = true;
        boolean isFood = true;
        
        try {
            
            // 냉장고 문을 연다.
            
            // 간식을 찾는다.
            
            // 간식이 없으면 예외 발생
            if(!isSnack) throw new SnackNotFoundException();
            
            // 우유가 없으면 예외 발생
            if(!isMilk) throw new MilkNotFoundException();
            
            // 먹을게 없으면 예외 발생
            if(!isFood) throw new FoodNotFoundException();
            
            // 간식을 꺼낸다.
            
            // 냉장고 문을 닫는다.
            
        } catch (SnackNotFoundException snfe) {
            
            // 간식이 없으면 실행하는 블록
            
            // 냉장고 문을 닫는다.
            
        } catch (MilkNotFoundException mnfe) {
            
            // 우유가 없으면 실행하는 블록
    
            // 냉장고 문을 닫는다.
            
        } catch (FoodNotFoundException mnfe) {
            
            // 먹을게 없으면 실행하는 블록
    
            // 냉장고 문을 닫는다.
            
        }
    }
}
 
class SnackNotFoundException extends Exception { }
class MilkNotFoundException extends Exception { }
class FoodNotFoundException extends Exception { }

 

try 블록 내에서 코드를 실행하는 도중 예외가 발생하면, try 블록 마지막에 실행하는 냉장고 문을 닫는 일을 할 수 없다.

먹고 싶은게 없다고 냉장고 문을 계속 열어두면, 등짝맞기 딱 좋다.

 

그래서 문을 닫아야 하는데, 모든 예외 상황에 문을 닫는다고 하려니 같은 내용이 반복 된다.

만약 냉장고 문을 그냥 닫지 않고, 이것좀 와서 보라고 한 다음에 냉장고 문을 닫고 싶으면 네곳을 고쳐야 한다.

슬픈일이 아닐 수 없다.

 

또 어쩌면 실수로 특정 예외 상황에서 냉장고 문을 닫는것을 깜빡하고 빠뜨릴 수도 있다.

등짝이 남아나지 않을거다.

 

 

728x90

 

 

 

예외가 발생해도 하지 않아도 무조건 냉장고 문을 닫아야 등짝이 터지지 않을 수 있기 때문에 finally 블록을 사용한다.

 

package me.xxxelppa.study.week09;
 
public class Exam_003 {
    public static void main(String[] args) {
        
        boolean isSnack = true;
        boolean isMilk = true;
        boolean isFood = true;
        
        try {
            
            // 냉장고 문을 연다.
            
            // 간식을 찾는다.
            
            // 간식이 없으면 예외 발생
            if(!isSnack) throw new SnackNotFoundException();
            
            // 우유가 없으면 예외 발생
            if(!isMilk) throw new MilkNotFoundException();
            
            // 먹을게 없으면 예외 발생
            if(!isFood) throw new FoodNotFoundException();
            
            // 간식을 꺼낸다.
            
        } catch (SnackNotFoundException snfe) {
            
            // 간식이 없으면 실행하는 블록
            
        } catch (MilkNotFoundException mnfe) {
            
            // 우유가 없으면 실행하는 블록
            
        } catch (FoodNotFoundException mnfe) {
            
            // 먹을게 없으면 실행하는 블록
            
        } finally {
            
            // 냉장고 문을 닫는다.
        }
    }
}
 
class SnackNotFoundException extends Exception { }
class MilkNotFoundException extends Exception { }
class FoodNotFoundException extends Exception { }

 

그리고 자연스럽게 넘어갔는데, 특정 예외를 강제로 발생하고 싶을 때 throw 를 사용할 수 있다.

이 throw 를 사용할 경우 크게 두 가지 방법으로 처리할 수 있다.

 

하나는 위에서 예시로 만든 것처럼 catch 블록을 사용해서 직접 후처리를 하는 것이고

다른 하나는 메소드에 throws 키워드를 사용해서 나를 호출한 녀석에게 예외를 처리 해달라고 던져버리는 것이다.

 

이 두번째 방법을 예외 전파라고도 한다.

 

package me.xxxelppa.study.week09;
 
public class Exam_004 {
    public static void main(String[] args) {
        try {
            myMethod_1();
        } catch (Exception e) {
            
            // 예외가 전파되어온 스택을 출력한다.
            e.printStackTrace();
        }
    }
    
    public static void myMethod_1() throws Exception {
        myMethod_2();
    }
    
    public static void myMethod_2() throws Exception {
        myMethod_3();
    }
    
    public static void myMethod_3() throws Exception {
        int a = 10;
        int b = 0;
        
        // 0으로 나누면 예외가 발생한다.
        a = a / b;
    }
}

 

java.lang.ArithmeticException: / by zero

at me.xxxelppa.study.week09.Exam_004.myMethod_3(Exam_004.java:25)
at me.xxxelppa.study.week09.Exam_004.myMethod_2(Exam_004.java:17)
at me.xxxelppa.study.week09.Exam_004.myMethod_1(Exam_004.java:13)
at me.xxxelppa.study.week09.Exam_004.main(Exam_004.java:6)

 

실행 결과를 보면 최초로 예외가 무엇 때문에 어디서 발생 했는지 call stack 을 출력해서 보여주고 있다.

 

 

개인적인 생각이지만 일반적으로 사람들은 똑같은 일을 반복하는걸 별로 좋아하지 않는다.

특히 개발자들은 더 그런것 같다.

 

예외를 처리할 때 try ~ with ~ resource 라는게 있다.

자바 7부터 등장했는데, 다음과 같이 생겼다.

 

package me.xxxelppa.study.week09;
 
public class Exam_005 {
    public static void main(String[] args) {
        
        boolean isSnack = true;
        boolean isMilk = true;
        boolean isFood = true;
        
        try (Refrigerator refrigerator = new Refrigerator()){
            
            // 냉장고 문을 연다.
            
            // 간식을 찾는다.
            
            // 간식이 없으면 예외 발생
            if(!isSnack) throw new SnackNotFoundException();
            
            // 우유가 없으면 예외 발생
            if(!isMilk) throw new MilkNotFoundException();
            
            // 먹을게 없으면 예외 발생
            if(!isFood) throw new FoodNotFoundException();
            
            // 간식을 꺼낸다.
            
        } catch (Exception e) {
            if(e instanceof SnackNotFoundException) {
                // 간식이 없으면 실행하는 블록
            } else if(e instanceof MilkNotFoundException) {
                // 우유가 없으면 실행하는 블록
            } else if(e instanceof FoodNotFoundException) {
                // 먹을게 없으면 실행하는 블록
            }
            
            e.printStackTrace();
        } 
    }
}
 
class Refrigerator implements AutoCloseable {
    @Override
    public void close() throws Exception {
        // 냉장고 문을 닫는다.
    }
}
 
class SnackNotFoundException extends Exception { }
class MilkNotFoundException extends Exception { }
class FoodNotFoundException extends Exception { }

 

10라인에 try 블록에 소괄호 블록이 생겼다.

이 소괄호 블록 안에서 생성한 객체는 finally 에서 후처리를 해주지 않아도 후처리를 해준다.

대신 조건이 있다.

 

41라인에 해당 클래스가 정의되어 있는 것을 보면, AutoCloseable 라는 인터페이스를 구현하고 있는것을 볼 수 있다.

이 인터페이스를 정의하고 있는 클래스라면 위와같이 조금은 더 간결하게 코드를 작성할 수 있다.

 

 

마지막으로 커스텀한 예외는 위에서 예제를 코드를 만들어 보면서 작성 했다.

 

바로 다음 절에서 Checked 와 Unchecked 예외에 대해 설명 하지만,

Checked 예외 상황을 만들 경우 Exception을 포함한 자식 타입 클래스를, Unchecked 예외 상황을 만들 경우 RuntimeException을 포함한 자식 타입 클래스를 상속 받으면 된다.

 

package me.xxxelppa.study.week09;
 
public class Exam_006 {
    public static void main(String[] args) throws MyException {
        
        throw new MyException("내가 만든 커스텀한 예외");
        
    }
}
 
class MyException extends Exception {
    
    // super 생성자를 사용하면
    // Exception 클래스의 부모인 Throwable 클래스의 생성자를 호출한다.
    
    public MyException() {
        super();
    }
    
    public MyException(String message) {
        super(message);
    }
}

 

Exception in thread "main" me.xxxelppa.study.week09.MyException: 내가 만든 커스텀한 예외

at me.xxxelppa.study.week09.Exam_006.main(Exam_006.java:6)

 

 

자바가 제공하는 예외 계층 구조, Exception과 Error의 차이는?, RuntimeException과 RE가 아닌 것의 차이는?


자바 api 문서를 참고해보면 모든 클래스의 최상위 부모인 Object 를 상속 받은 Throwable 을 상속 받은 Exception 과 Error 가 있다.

 

Error는 메모리가 부족하다던가 등의 JVM에 문제가 생겼을 경우에 발생하고,

Exception은 프로그램 코드 레벨에서 문제가 생겼을 경우에 발생하는 편이다.

 

Exception 은 또 크게 두가지로 구분해서 생각할 수 있는데, Checked 와 Unchecked 이다.

 

검색해보니 잘 정리된 좋은 그림이 있어서 가져와보았다.

 

** 출처 : https://madplay.github.io/post/java-checked-unchecked-exceptions

 

 

Unchecked 예외는 RuntimeException (런타임 예외) 라고도 하는데, 보통 프로그램 실행중에 발생하며, 발생할 것을 미리 알 수 없다는 특징이 있다.

그리고 이 예외는 Exception 을 상속 받은 RuntimeException 을 상속 받은 클래스이다.

 

 

반면 Checked 예외는 RuntimeException이 아닌 예외를 말한다.

 

이러한 예외는 프로그램 실행중에 갑자기 발생하는 것이 아니기 때문에 미리 대처해둘 수 있다는 특징이 있다.

그렇기 때문에 RE가 아닌 (Unchecked Exception이 아닌) 예외에 대해서는 코드에서 명시적으로 예외 처리를 해주어야 한다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

11주차 : Enum  (0) 2021.05.01
10주차 : 멀티쓰레드 프로그래밍  (0) 2021.05.01
8주차 : 인터페이스  (0) 2021.05.01
7주차 : 패키지  (0) 2021.05.01
6주차 : 상속  (0) 2021.05.01
728x90

# 자바의 인터페이스에 대해 학습하세요.


# 학습할 것

  • 인터페이스를 정의하는 방법
  • 인터페이스를 구현하는 방법
  • 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  • 인터페이스 상속
  • 인터페이스의 기본 메소드 (Default Method), 자바 8
  • 인터페이스의 static 메소드, 자바 8
  • 인터페이스의 private 메소드, 자바 9

본격적으로 인터페이스에 대해 정리하기에 앞서 인터페이스가 무엇인지 정리해보려 한다.

 

극단적으로 얘기하면 인터페이스는 구현을 강제하도록 하기 위한 것이다.

그러면 불편한게 아닐까 싶은 생각도 있겠지만, 오히려 그 반대라고 생각하면 된다.

생활 속에서 인터페이스의 예를 찾아보면 콘센트가 인터페이스라고 할 수 있다.

 

적어도 한국에서 사용하는 콘센트의 규격이 정해져있기 때문에

우리는 전자제품을 살 때 그 제품이 콘센트에 연결이 되는 것인지 고민 없이 살 수 있다.

(어떻게 보면 JVM 도 인터페이스 같이 느껴진다.)

 

해외 여행을 다녀봤다면 플러그 어댑터를 한 번쯤은 생각해 봤을 거다.

그 이유는 다른 나라의 콘센트 규격이 우리와 다르기 때문이다.

즉, 사용하는 인터페이스가 달라서 그 인터페이스를 사용하기 위해 새로운 인터페이스를 준비했다고 볼 수 있다.

 

 

이 인터페이스는 큰 특징이 있다.

java 1.8 이전 버전을 기준으로, 인터페이스는 두가지만 가질 수 있었다.

public static final 의 성격을 가지는 변수와 public abstract 의 성격을 가지는 구현 없이 정의만 있는 메소드이다.

 

만약 그냥 생략하고 작성 한다고 해도 소용없다.

생략하면 위에 얘기한 타입이 자동으로 붙고, 다른 타입을 사용하려하면 컴파일 오류가 발생한다.

 

package me.xxxelppa.study.week08;
 
public interface Exam_001 {
    /*
     * ERROR CASE
     * 
     * public static final int test_01;        // Variable 'test_01' might not have been initialized
     * protected final int test_02 = 10;       // Modifier 'protected' not allowed here
     * private static final int test_03 = 10;  // Modifier 'private' not allowed here
     * 
     * private void mTest_01();    // Modifier 'private' not allowed here
     * protected void mTest_02();  // Modifier 'protected' not allowed here
     * void mTest_03() { }         // Interface abstract methods cannot have body
     */
    
    
    /*
     * CAN USE CASE
     */
    int test_01 = 10;
    static int test_02 = 10;                // Modifier 'static' is redundant for interface fields
    final int test_03 = 10;                 // Modifier 'final' is redundant for interface fields
    public static int test_04 = 10;         // Modifier 'public' is redundant for interface fields, Modifier 'static' is redundant for interface fields
    public final int test_05 = 10;          // Modifier 'public' is redundant for interface fields, Modifier 'final' is redundant for interface fields
    public static final int test_06 = 10;   // Modifier 'public' is redundant for interface fields, Modifier 'static' is redundant for interface fields, Modifier 'final' is redundant for interface fields
    
    void mTest_03();
    abstract void mTest_02();               // Modifier 'abstract' is redundant for interface methods
    public abstract void mTest_01();        // Modifier 'public' is redundant for interface methods, Modifier 'abstract' is redundant for interface methods
    
}

 

 

인터페이스를 정의하는 방법


인터페이스는 다음과 같이 정의할 수 있다.

 

package me.xxxelppa.study.week08;
 
public interface Exam_002 {
}

 

public 이외의 다른 접근 제한자를 사용하면 오류가 발생한다.

생략할 수는 있다.

 

 

728x90

 

 

 

인터페이스를 구현하는 방법, 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법


인터페이스는 기본적으로 추상 메소드만 가지고 있기 때문에 어디선가 누군가는 구현을 해주어야 한다.

특정 interface를 구현하기 위해서는 클래스가 필요하고, 이 클래스는 구현 할 인터페이스를 상속 받아야 한다.

 

** 되도록이면 클래스나 인터페이스는 각자 독립된 파일에 작성하지만, 보기 편하기 위해 하나의 파일에 작성했다.

** 그리고 사실 인터페이스도 .java 확장자를 가지며, 컴파일하면 .class 파일이 생성 된다 !

 

앞서 인터페이스는 구현을 강제 한다고 했는데, 예를 들면 다음과 같이 할 수 있다.

 

하나의 예제에 너무 많은걸 넣은것 같지만, 천천히 보면 쉬운 내용이다.

 

package me.xxxelppa.study.week08;
 
interface MyInterface_01 {
    void mustBeInvoked();
}
 
public class Exam_003 {
    public static void main(String[] args) {
        
        MyTestClass_01 mtc_01 = new MyTestClass_01();
        MyTestClass_02 mtc_02 = new MyTestClass_02();
        
        /*
         * mtc_01 과 mtc_02 변수는 각각 특정 클래스의 객체를 담고 있다.
         * 이 두 클래스는 모두 MyInterface_01 이라는 인터페이스를 상속받았으므로
         * MyInterface_01 에 정의한 추상 메소드인 mustBeInvoked() 가 무조건 구현 되어있음을 알 수 있다.
         *
         * 그리고 정말 중요한 한가지가 더 있는데,
         * 두 클래스가 같은 인터페이스를 상속 받았기 때문에 다음과 같이 배열로 관리할 수 있다.
         */
        
        // 인터페이스 타입으로 묶어서 관리할 수 있다 !!!
        MyInterface_01[] mi_01 = {
                new MyTestClass_01()
                , new MyTestClass_02()
        };
        
        // 같은 인터페이스를 구현하고 있기 때문에
        // mustBeInvoked 메소드가 있음을 확신하고 사용할 수 있다.
        for (MyInterface_01 mi : mi_01) {
            mi.mustBeInvoked();
        }
    }
}
 
/*
 * 인터페이스를 상속 받았는데,
 * 추상 메소드를 구현하지 않으면 (즉, 오버라이딩 하지 않으면)
 * 다음과 같은 컴파일 오류를 발생한다.
 *
 * Class 'MyTestClass_01' must either be declared abstract or implement abstract method 'mustBeInvoked()' in 'MyInterface_01'
 */
class MyTestClass_01 implements MyInterface_01 {
    
    /*
     * @Override 어노테이션을 생략해도 상관 없지만
     * 오버라이딩 한 메소드임을 컴파일러에게 알려주고
     * 사용자 입장에서도 한 눈에 오버라이딩 한 메소드임을 알려주기 위해
     * 굳이 사용하는 것을 권장한다.
     */
    @Override
    public void mustBeInvoked() {
        System.out.println("DO SOMETHING!");
    }
}
 
class MyTestClass_02 implements MyInterface_01 {
    @Override
    public void mustBeInvoked() {
        System.out.println("DOBBY IS FR22");
    }
}

 

DO SOMETHING!
DOBBY IS FR22

 

위 예제를 온전히 이해한다면, 인터페이스에서 가장 빈번하게 사용하는 개념을 알고 있다고 해도 과언이 아니라고 생각한다.

 

 

 

인터페이스 상속


인터페이스 사용을 정말 권하는? 이유가 있는데, 다중 상속 문제이다.

인터페이스간의 상속이 아닌 클래스의 상속에 대해 되짚어 생각해보면,

클래스는 다이아몬드 상속 문제로 다중 상속이 불가능 하다.

 

하지만, 인터페이스는 클래스에 다중 상속이 가능하다.

 

package me.xxxelppa.study.week08;
 
public class Exam_004 implements MyInterface_02, MyInterface_03, MyInterface_04 {
    
}
 
interface MyInterface_02 { }
interface MyInterface_03 { }
interface MyInterface_04 { }

 

서로 다른 인터페이스에 같은 추상 메소드가 있어도 문제없다.

왜냐면 어차피 구현되어있지 않기 때문에, 똑같은 메소드를 상속 받아도 한 번만 구현하면 문제 없기 때문이다.

 

package me.xxxelppa.study.week08;
 
public class Exam_005 implements MyInterface_05, MyInterface_06 {
    @Override
    public void test() {
        System.out.println("한 번만 오버라이딩 하면 된다.");
    }
}
 
interface MyInterface_05 { void test(); }
interface MyInterface_06 { void test(); }

 

 

 

인터페이스의 기본 메소드 (Default Method), 자바 8


인터페이스는 자바 1.8 부터 기능이 확장 되었다.

이전에는 '인터페이스 구현 메소드 가지고 있는 소리 하네' 하는 농담이 가능했지만 앞으로는 그러면 안된다.

바로 이 default 메소드가 구현부를 가지는 메소드이기 때문이다.

 

그럼 이게 왜 생겼을까?

적어도 자바를 쓰는 사람 혼란스러우라고 만든건 아니다.

 

인터페이스를 잘 생각 해보면, 인터페이스를 상속 받은 클래스는 인터페이스 내부에 정의한 추상 메소드 구현을 해야만 한다고 했다.

여기까지는 좋은데, 만약 특정 인터페이스를 구현하고 있는 클래스가 100개가 있는데 이 인터페이스에 추상 메소드 하나를 더 추가 해야 한다고 해보자.

 

새롭게 추가된 추상 메소드를 사용하는 클래스도 있겠지만, 사용하지 않아도 되는 클래스도 분명 있을 것이다.

그러면 사용하지 않는 클래스에서는 울며 겨자먹기로 아무것도 하지 않지만 비어있는 실행 블록을 작성 해야만 하는 상황에 놓일 수 있다.

끔찍한 일이 아닐 수 없다.

 

이런 상황에 클래스에서 인터페이스 내부의 메소드를 선택적으로 오버라이딩 해서 사용할 수 있도록

즉, 굳이 오버라이딩 하지 않아도 컴파일 오류가 발생하지 않는 메소드를 인터페이스에 추가하기 위해서 만든 것이 default 메소드 이다.

 

사용 방법은 간단하다.

 

package me.xxxelppa.study.week08;
 
public class Exam_006 implements MyInterface_07 {
    // 인터페이스 내부 메소드를 오버라이딩 하지 않았지만 오류가 발생하지 않는다.
    
    public void doSomething() {
        // 이렇게 호출해서 사용할 수도 있다.
        myTestMethod();
    }
}
 
interface MyInterface_07 {
    default void myTestMethod() {
        System.out.println("인터페이스도 구현 메소드를 가질 수 있다.");
    }
}

 

 

 

인터페이스의 static 메소드, 자바 8


default 메소드와 마찬가지로 static 메소드도 자바 1.8부터 생겼다.

자바에서 static 이랑 키워드를 만나면, 일단 동적이지 않고 정적이라는 생각을 하면서 객체를 만들지 않고 사용할 수 있다고 생각 하면 좋다.

일단 static 은 메모리에 로드 되는 시점이 다르다.

new 연산자를 사용하면 런타임에 동적으로 객체가 생성되어 메모리에 로드 되는 것이 아니고, 클래스가 로드 되는 시점에 생성 된다.

그렇기 때문에 static 으로 선언된 것들은 ​객체를 생성하지 않고 바로 사용할 수 ​있다.

 

클래스의 멤버 필드나 멤버 메소드의 경우 static 키워드를 사용하면, 해당 클래스 이름으로 객체 생성 없이 바로 사용할 수 있다.

 

package me.xxxelppa.study.week08;
 
public class Exam_007 {
    public static void main(String[] args) {
        System.out.println(" :: " + MyStaticTestClass.static_value);
        MyStaticTestClass.static_method();
        
        // Non-static field 'non_static_value' cannot be referenced from a static context
        // System.out.println(" :: " + MyStaticTestClass.non_static_value);
        
        // Non-static method 'non_static_method()' cannot be referenced from a static context
        // MyStaticTestClass.non_static_method();
    }
}
 
class MyStaticTestClass {
    public int non_static_value = 10;
    public static int static_value = 20;
    
    public void non_static_method() { }
    public static void static_method() { }
}

 

조금 다른 길로 새어나갔는데, 인터페이스의 static 메소드도 클래스의 멤버에 static 키워드를 붙인 것과 마찬가지로

인터페이스 이름으로 바로 접근해서 사용할 수 있다.

 

package me.xxxelppa.study.week08;
 
public class Exam_008 {
    public static void main(String[] args) {
        MyInterface_08.my_static_method();
    }
}
 
interface MyInterface_08 {
    static void my_static_method() {
        System.out.println("static method 입니다.");
    }
}

 

인터페이스의 static 메소드 관련해서 재밌는것이 하나 더 있다.

바로 오버라이딩 할 수 없다는 것이다.

 

package me.xxxelppa.study.week08;
 
public class Exam_009 implements MyInterface_09 {
    
    // @Override    // Method does not override method from its superclass
    static void my_static_method() {
        System.out.println("오버라이딩 하고 싶은데..");
    }
    
    public static void main(String[] args) {
        my_static_method();
        MyInterface_09.my_static_method();
    }
}
 
interface MyInterface_09 {
    static void my_static_method() {
        System.out.println("오버라이딩 할 수 없습니다.");
    }
}

 

오버라이딩 하고 싶은데..
오버라이딩 할 수 없습니다.

 

오버라이딩이 불가능한 이유에 대해 찾아보았는데, 다음과 같은 내용을 찾아낼 수 있었다.

 

Overloading is the mechanism of binding the method call with the method body dynamically based on the parameters passed to the method call.
Static methods are bonded at compile time using static binding. Therefore, we cannot override static methods in Java.

Unlike other methods in Interface, these static methods contain the complete definition of the function and since the definition is complete and the method is static, therefore these methods cannot be overridden or changed in the implementation class.

 

 

인터페이스의 private 메소드, 자바 9


자바 9에서는 인터페이스에 private 메소드를 사용할 수 있게 되었다.

private 키워드를 사용해서 일반 메소드와 static 메소드 두 가지를 정의할 수 있는데

평범한 클래스에서 (plain old java class .. ?????) 사용하는 것과 같다고 생각하면 된다.

 

예를 들면 static 메소드에서 non static 메소드를 사용할 수 없다던가 하는 것들이다.

 

package me.xxxelppa.study.week08;
 
public class Exam_010 {
 
}
 
interface MyInterface_10 {
    default void my_default_method() {
        // private 메소드 사용 가능
        my_private_method();
        System.out.println("my default method");
    }
    
    private void my_private_method() {
        // static 메소드 사용 가능
        my_private_static_method();
        System.out.println("my private method");
    }
    
    static void my_static_method() {
        // 다른 static 메소드 사용 가능
        my_another_private_static_method();
        System.out.println("my static method");
    }
    
    static void my_another_static_method() {
        // 다른 일반 메소드 사용 불가능
        // non-static method my_private_method() cannot be referenced from a static context
        // my_private_method();
    }
    
    private static void my_private_static_method() {
        // 다른 static 메소드 사용 가능
        my_static_method();
        System.out.println("my private static method");
    }
    
    private static void my_another_private_static_method() {
        System.out.println("my another private static method");
    }
}

 

정말 마지막으로 인터페이스에 대해 딱 하나만 더 얘기 한다면,

다른건 몰라도 전략 패턴에 대해서는 꼭 한번 찾아봤으면 좋겠다.

 

 

 

 

728x90

'Archive > Java online live study S01' 카테고리의 다른 글

10주차 : 멀티쓰레드 프로그래밍  (0) 2021.05.01
9주차 : 예외 처리  (0) 2021.05.01
7주차 : 패키지  (0) 2021.05.01
6주차 : 상속  (0) 2021.05.01
5주차 : 클래스  (0) 2021.05.01

+ Recent posts