5 분 소요

다음에 나올 기능들은 서로 관련이 없는 기능들이 많다. 그래서 이들을 하나의 카테고리로 묶어 부를 적절한 용어가 없어서 이 페이지 제목을 “자바 고급 기능들”이라고 지었다.

enum : 열거형. 같은 분류의 상수 모음.

어떤 햄버거 가게가 있는데, 메인 요리와 음료수로 나누고, 각 요리 또는 음료마다 고유한 번호를 붙여 관리하고자 한다. 자바에서 다음과 같이 interface 내부에 상수를 정의함으로써 메인 요리와 음료수로 나눠 분류하였다.

interface MainFoodCode {
    int HAMBURGER = 10;
    int POTATOSTICK = 15;
}

interface Beverages {
    int COLA = 10;
    int MILK = 15;
}

이제 손님의 주문을 받아보자. 손님은 햄버거 한 개와 콜라 한 개를 주문하였다. 이 주문을 처리하기 위해 다음의 코드를 짜고 실행시켜보았다.

interface MainFoodCode {
    int HAMBURGER = 10;
    int POTATOSTICK = 15;
}

interface Beverages {
    int COLA = 10;
    int MILK = 15;
}

class ProblemWithInterface {
    public static void printYourMainOrder(int menu) {
        String yourMenu = "";
        switch(menu) {
            case MainFoodCode.HAMBURGER:
                yourMenu = "햄버거";
                break;
            case MainFoodCode.POTATOSTICK:
                yourMenu = "감자튀김";
                break;
        }
        System.out.printf("주문하신 %s 나왔습니다!\n", yourMenu);
    }

    public static void main(String[] args) {
        printYourMainOrder(MainFoodCode.HAMBURGER);
        printYourMainOrder(Beverages.COLA);
    }
}

주문하신 햄버거 나왔습니다!
주문하신 햄버거 나왔습니다!

이럴 수가! 나오라는 콜라는 안 나오고 햄버거만 2개가 나와버렸다. 손님이 매우 화를 내며 불평을 낸다! 이게 어찌된 일일까?

interface를 이용하여 서로 같은 카테고리로 묶이는 상수들을 모아 정의할 때 여러 interface 간의 일부 상수들의 값이 서로 겹치면 엉뚱한 결과가 나오는 것이다. 메인 메뉴 상수만을 받아드리는 목적의 printYourMainOrder 메서드는 사실 MainFoodCode 인터페이스뿐만 아니라 Beverages 인터페이스의 상수도 받아들일 수 있다. 이로 인해 메인 요리만 취급하는 메서드에 음료수 인터페이스 상수를 대입해도 에러 발생없이 그대로 실행되는 문제가 발생하는 것이다.

이렇게 분류가 다른 상수들을 각자 그룹화할 때의 상수값이 서로 겹치는 문제를 해결할 수 있는 자바 문법으로 enum이 있다. 하나의 카테고리를 공통으로 가지고 있는 상수들을 모아놓은 그룹을 열거형이라 한다. 예를 들어 요일이란 이름의 열거형 안에는 월요일=1, 화요일=2, … , 일요일=7 과 같이 모두 요일을 공통 분모로 갖는 상수들을 모아 정의할 수 있다. 열거형 enum의 사용 형태는 다음과 같다.

enum 열거형이름 {
    상수1, 상수2, ...
}
enum MainFoodCode2 {HAMBURGER, POTATOSTICK}
enum Beverages2 {COLA, MILK}

열거형 내부에 정의된 각 상수들을 요소라 부르며, 요소에 접근할 때에는 열거형.요소 와 같이 마침표 연산자를 이용하면 된다.

enum 내부에 정의된 상수들은 첫 요소부터 순서대로 0, 1, … 처럼 자동으로 그 숫자가 부여된다.

enum은 클래스 내부에 정의하여 사용할 경우, 해당 클래스 내부에서만 사용 가능하다.

enum 내 상수들은 Beverage.COLA == 0 과 같은 비교 연산이 불가능하다. 앞서 언급했듯, enum은 클래스이기 때문에 객체와 기본 자료형을 비교하는 셈이기 때문이다.

enum은 클래스로 취급되어서 메서드의 매개변수의 자료형으로도 사용할 수 있다. 바로 이 점을 이용하여 앞선 상수 겹침 문제를 해결할 수 있다. 즉 MainFoodCode 자료형 매개변수를 선언하면 해당 열거형의 상수만 인자로 전달할 수 있는 것이다.

가게에서 생긴 문제를 enum을 이용하여 해결해보자.

enum MainFoodCode2 {HAMBURGER, POTATOSTICK}
enum Beverages2 {COLA, MILK}

class solveWithEnum {
    public static void printYourMainOrder(MainFoodCode2 menu) {
        String yourMenu = "";
        switch(menu) {
            case HAMBURGER:
                yourMenu = "햄버거";
                break;
            case POTATOSTICK:
                yourMenu = "감자튀김";
                break;
        }
        System.out.printf("주문하신 %s 나왔습니다!\n", yourMenu);
    }

    public static void printYourBeverageOrder(Beverages2 bev) {
        String yourMenu = "";
        switch(bev) {
            case COLA:
                yourMenu = "콜라";
                break;
            case MILK:
                yourMenu = "우유";
                break;
        }
        System.out.printf("주문하신 %s 나왔습니다!\n", yourMenu);
    }

    public static void main(String[] args) {
        printYourMainOrder(MainFoodCode2.HAMBURGER);
        // printYourMainOrder(Beverages.COLA);  // 에러

        printYourBeverageOrder(Beverages2.COLA);

        System.out.println(MainFoodCode2.HAMBURGER instanceof Enum);
    }
}

주문하신 햄버거 나왔습니다!
주문하신 콜라 나왔습니다!
true

행복한 한 끼 되세요!

가변 인수

가변 인수(Variable length argument)는 메서드로 전달되는 인수의 개수가 가변적인 인수를 말한다. 이 가변 인수를 메서드의 매개변수로 설정하면 상황마다 인수를 개수를 달리 하여 메서드에 전달할 수 있게 된다. 메서드 매개변수에 가변 인수를 정의하고자 한다면 자료형 뒤에 마침표 3개(…)를 붙인다.

만약 메서드 매개변수에 가변 인수가 아닌 인수와 함께 사용한다면 가변 인수는 무조건 마지막에 나와야 한다.

컴파일러는 가변 인수를 배열로 처리한다.

다음은 메서드의 매개변수에 가변 인수를 적용한 예이다.

class VariableLengthArgs {
    public static int getAdd(int... args) {
        int total = 0;
        for(int num: args) {
            total += num;
        }
        return total;
    }
    
    public static void main(String[] args) {
        System.out.println(getAdd(1));
        System.out.println(getAdd(1, 2));
        System.out.println(getAdd(1, 2, 3));
    }
}

1
3
6

어노테이션 (Annotation)

자바에서 어노테이션(annotation)은 코드에 대한 정보, 설명을 추가해주는 메타 데이터이다. “@” 기호를 앞에 붙여 이용한다. 어노테이션에는 다음이 존재한다.

@Override

해당 어노테이션은 오버라이딩할 메서드명이 제대로 적혀있는지 컴파일러가 체크하는 용도이다.

class User6 {
    String nickName;

    User6(String nickName) {
        this.nickName = nickName;
    }

    @Override
    public String toString() {
        return nickName;
    }
}

class AnnoOveride {
    public static void main(String[] args) {
        User6 me = new User6("good123");

        System.out.println(me);
    }
}

위 예제에서 User6 클래스 내부에서 toString 메서드를 오버라이딩하고 있다. 이 때 @Override 어노테이션이 적용되어 있다. 여기서 일부러 해당 메서드명에 오타를 낸 뒤 javac 명령어로 컴파일을 시도하면 다음의 메세지가 뜨면서 컴파일에 실패한다.

AnnoOveride.java:8: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

이를 통해 오버라이딩하는 메서드명을 제대로 적었는지 확인할 수 있다.

@Deprecated

프로그래밍에서, 호환성을 위해 아직 기능이 유지되고 있지만, 어떤 이슈가 있거나 개선된 버전의 다른 기능이 개발되어 필요가 없어져 사라질 수 있는 기능을 deprecated되었다고 표현한다. 보통은 이러한 기능에 대해 “해당 기능은 오래되어 추후 사라질 수 있는 기능입니다”라는 식의 메시지를 달아 사용자에게 주의를 주곤 한다. 자바에서는 이러한 행위를 @Deprecated 어노테이션을 통해 실현할 수 있다.

다음은 개선된 버전의 메서드가 개발되어 기존 메서드가 추후에 사라질 수도 있음을 알리기 위해 해당 어노테이션을 적용한 예제 코드이다.

class User7 {
    String userId;
    int point;

    User7(String userId, int point) {
        this.userId = userId;
        this.point = point;
    }

    @Deprecated
    void printProfile() {
        System.out.println(this.userId);
        System.out.println(this.point);
    }

    void showProfile() {
        System.out.println("====== 사용자 정보 ======");
        System.out.println("사용자 아이디 : " + userId);
        System.out.println("보유 포인트 : " + point);
        System.out.println("========================");
    }
}

class DeprecatedAnno {
    public static void main(String[] args) {
        User7 me = new User7("good123", 1000);

        me.printProfile();
        me.showProfile();
    }
}

위 코드 파일을 명령 프롬프트 창에서 javac 명령어를 이용하여 클래스 파일로 생성하려고 하면 다음의 메시지를 받게 될 것이다.

사진 4-1.

사진 4-1.

Deprecated된 메서드를 사용한다고 해서 문제가 되는 건 아니지만, 컴파일 과정에서 해당 메서드가 deprecated 되었다는 메시지가 뜨는 것을 볼 수 있다.

또한 vscode 에디터 상에서도 해당 어노테이션이 적용된 메서드를 호출하려고 하면 다음과 같이 해당 메서드명에 취소선이 그어진다.

20240321_16225646.png

@SuppressWarnings

해당 어노테이션은 경고 메시지를 출력하지 않도록 해주는 어노테이션이다. 해당 어노테이션 뒤의 괄호 안에 특정 문자열을 입력하면 그에 해당하는 경고 메시지를 출력하지 않게 해준다. 다음의 예제에서는 Deprecated 어노테이션이 적용된 메서드에 대해 해당 경고 메시지가 뜨지 않도록 SuppressWarings 어노테이션과 함께 “deprecation” 문자열이 지정되었다.

class User8 {
    String userId;
    int point;

    User8(String userId, int point) {
        this.userId = userId;
        this.point = point;
    }

    @Deprecated
    //@SuppressWarnings("deprecation")
    void printProfile() {
        System.out.println(this.userId);
        System.out.println(this.point);
    }

    void showProfile() {
        System.out.println("====== 사용자 정보 ======");
        System.out.println("사용자 아이디 : " + userId);
        System.out.println("보유 포인트 : " + point);
        System.out.println("========================");
    }
}

class SuppressWarningsAnno {
    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        User8 me = new User8("good123", 1000);

        me.printProfile();
        me.showProfile();
    }
}

위 코드를 명령 프롬프트 창에서 javac 명령어를 이용하여 컴파일을 해보면 deprecated 관련 경고 메시지가 뜨지 않는다.

사진 5-1

사진 5-1

해당 어노테이션을 사용할 때에는 @Deprecated 어노테이션과 같이 경고 메시지를 출력하는 다른 어노테이션이 직접 적용된 메서드 정의부에 같이 사용하면 효과가 없다. (위 코드의 주석 처리된 부분을 주석 해제하여 실행해보면 경고 메시지가 그대로 나온다) 따라서 이 경우, 해당 메서드를 호출하는 다른 메서드에 @SuppressWarnings 어노테이션을 적용하거나, 아니면 다음과 같이,

interface SiteUser {
    @Deprecated
    void printProfile();
    void showProfile();
}

class User8 implements SiteUser {
    // 생략
    @SuppressWarnings("deprecation")
    void printProfile() {
        System.out.println(this.userId);
        System.out.println(this.point);
    }
    // 생략
}
// 생략

인터페이스에서 특정 메서드에 Deprecation과 같이 경고 메시지를 발생시키는 어노테이션을 작성하고, 해당 인터페이스를 구현하는 클래스 내 메서드에서 SuppressWarnings를 적용하면 된다.


References

[1] 이재환, “이재환의 자바 프로그래밍 입문”, (골든래빗, 2021)

This content is licensed under CC BY-NC 4.0

댓글남기기