13장. 인터페이스와 추상클래스, enum

공부하는 감자·2023년 12월 5일
0

자바의 신 3판

목록 보기
13/30

들어가기 전

『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처

방법론

자바에서 .class 파일을 만들 수 있는 것에는 아래와 같은 것들이 있다.

  • 클래스
  • interface
  • abstract 클래스

인터페이스와 abstract 클래스에 대해서 제대로 이해하려면, 먼저 시스템을 만드는 절차가 어떻게 되는지 알아야 한다.

어떠 시스템을 개발하든 간에 “방법론”이라는 것을 사용하여 개발한다. 방법론이라는 것은 시스템을 어떻게 만들 것인지에 대한 절차를 설명하고, 어떤 문서(산출물)를 작성해야 하는지를 정리해 놓은 공동 절차다.

일반적인 절차는 다음과 같다.

  • 분석
  • 설계
  • 개발 및 테스트
  • 시스템 릴리즈

분석

시스템을 분석하는 단계다.

시스템을 만들어 달라고 한 사람들(SI에서는 고객, SM에서는 현업, 고객이 별도로 없는 회사에서는 기획)에게 어떻게 개발하기를 원하는지 물어본다.

이러한 일련의 과정을 요구사항 분석이라고 한다. 물론 이 단계에서 하는 일은 여러 가지지만, 가장 주된 작업이 요구사항 분석이라고 보면 된다.

설계

분석 단계에서 만든 대략적인 그림을 프로그램으로 만들 수 있도록 설계하는 작업을 수행한다.

이 단계에서 어떤 메소드를 만들 것인지, 데이터는 어떻게 저장할지 등등의 세부적인 것들을 정리한다.

개발 및 테스트

설계에서 만들기로 한 것들을 개발하는 단계다.

실제 시스템에서 제공해야 하는 기능들을 이때 만든다. 이 만드는 작업을 개발이라고 하고, 필요한 기능들이 제대로 동작하는지 확인하는 테스트 작업을 수행한다.

시스템 릴리즈

시스템을 사용자들에게 제공하는 단계다.

시스템을 오픈한 이후에는 운영/유지보수 단계를 거치면서 문제가 있는 부분들을 고쳐나간다.

Interface와 Abstract 클래스를 사용해야 하는 이유

방법론의 설계 단계에서는 어떤 클래스를 만들지, 어떤 메소드를 만들지, 어떤 변수를 만들지를 정리하는 작업도 같이 한다. 그런데 나중에 관련 내용들이 변경될 수도 있다.

그러면 정리한 문서도 수정해야 하므로 2중 3중의 일이 된다.

이때, 설계 단계에서 인터페이스라는 것을 만들면 개발할 때 메소드 이름을 어떻게 할지, 매개 변수를 어떻게 할 지를 일일이 고민하지 않아도 된다.

abstract 메소드로 선언과 구현을 분리

인터페이스와 abstract 클래스는 다음과 같이 몸통이 없는 메소드를 선언하여 사용하는데, 이를 abtract 메소드라고 부른다.

public boolean equals(Object o)

개발자들은 이 인터페이스가 구현된 클래스에서 이 메소드를 호출하고 그 결과를 받을 것이다.

여기서 중요한 것은 “인터페이스가 구현된 클래스”가 어떻게 되어 있는지의 여부이다.

해당 메소드를 사용하는 사용자의 입장에서는 내부 구현이 어떻게 되어 있는지 몰라도 원하는 메소드를 호출하고, 그 답을 받으면 된다.

  • 예를 들어, 우리가 노트북, PC, TV의 전원 버튼을 누르면 어떤 원리에 따라서 전자기기들이 켜지는지 알 수가 없다.

  • 가장 일반적인 것은 DAO(Data Access Object)라는 패턴이다. 이 패턴은 데이터를 저장하는 저장소에서 원하는 값을 요청하고 응답을 받는다.

    • 어떠한 DBMS를 사용해도 상관 없도록 메소드를 만든다.
    • Oracle을 사용하든, MongoDB를 사용하든 간에 이 인터페이스를 구현해서 작성한 메소드에서 결과만 제대로 넘겨주면 된다.

이렇게, 인터페이스를 정해 놓으면 선언과 구현을 구분할 수 있다.

💡 DBMS란
Database Management System의 약자로 데이터를 저장하는 저장소의 하나이다.

요약

  • 설계 시 선언해 두면 개발할 때 기능을 구현하는 데에만 집중할 수 있다.
  • 개발자의 역량에 따른 메소드의 이름과 매개 변수 선언의 격차를 줄일 수 있다.
  • 공통적인 인터페이스와 abstact 클래스를 선언해 놓으면, 선언과 구현을 구분할 수 있다.

abstract 메소드

abstract 메소드는 인터페이스 혹은 abstract 클래스 내에 만들어둔 몸통이 없는 메소드를 말한다.

인터페이스나 abstract 클래스를 구현할 경우에는 반드시 abstract 메소드들의 몸통을 만들어 주어야만 한다. 즉, 메소드들을 구현해야만 한다.

인터페이스의 예

명시적으로 인터페이스를 구현한 메소드라는 것을 표시하기 위해 @Override 를사용했다.

public interface MemberManager {
    public boolean addMember(MemberDTO member);
    public boolean removeMember(String name, String phone);
    public boolean updateMember(MemberDTO member);
}
public class MemberManagerImpl implements MemberManager {
    @Override
    public boolean addMember(MemberDTO member) {
        return false;
    }
    @Override
    public boolean removeMember(String name, String phone) {
        return false;
    }
    @Override
    public boolean updateMember(MemberDTO member) {
        return false;
    }
}

인터페이스 Interface

실제 코드는 만들지 않더라도 어떤 메소드들이 있어야 하는지를 정의하려고 할 때 인터페이스를 사용한다.

선언

인터페이스는 다음과 같이 사용한다.

public interface 인터페이스이름 {
	public 리턴타입 메소드();
	...
}

class 대신 interface로 시작하며, 내부에 선언된 메소드들은 몸통(body)이 있으면 안 된다.

구현

인터페이스를 적용하려면 클래스 선언문에서 클래스 이름 뒤에 implements 라는 예약어를 쓴 후, 그 뒤에 인터페이스들을 나열한다.

public class Impl implements 인터페이스이름 {
}

implements 는 “구현한다”는 말이다. 자바에서는 인터페이스를 implements 하면 “상속한다”가 아닌 “구현한다”고 표현한다.

즉, 해당 클래스에서 구현해야 하는 인터페이스들을 정의함으로써 클래스에 짐을 지어 주는 것이다. 자바는 상속은 하나밖에 할 수 없지만, 인터페이스는 여러 개를 구현할 수 있다.

💡 다중상속
상속은 다중 상속되면 안 되지만, 인터페이스는 내부에 구현된 메소드가 없기 때문에 다중 상속될 수 있다. (아래 '질문'에서 자세히 설명)

인터페이스의 용도

  • 효율적인 산출물 관리
    • 설계 단계에서 인터페이스만 만들어 놓고, 개발 단계에서 실제 작업을 수행하는 메소드를 만들면 설계 단계의 산출물과 개발 단계의 산출물이 보다 효율적으로 관리된다.
  • 외부에 노출되는 것을 정의해 놓고자 할 때 사용

이 중 두번째 용도는 구현체(MemberManagerImpl)가 아닌, 인터페이스(MemberManager)를 통해서 접근하는 것을 말한다.

// 인터페이스
public interface MemberManager {
    public boolean addMember(MemberDTO member);
}

// 구현
public class MemberManagerImpl implements MemberManager {
    @Override
    public boolean addMember(MemberDTO member) {
        return false;
    }
}

public class InterfaceExample {
    public static void main(String args[]) {
    	// 인터페이스로 접근
        MemberManager member=new MemberManagerImpl();
        
        // 구현된 것이 없으므로 아래처럼은 사용할 수 없다.
        // MemberManager member=new MemberManager();
    }
}

MemberManagerImpl 에는 MemberManager 에 선언되어 있는 모든 메소드들이 구현되어 있다. 따라서 위와 같이 선언해 실행하여 사용할 수 있다.

static 메소드과 final 메소드 사용

인터페이스에서는 static이나 final 메소드가 선언되어 있으면 안된다.

💡 아래 기술한 내용은 인터넷에서 검색하여 덧붙인 내용입니다.

인터페이스의 멤버 변수

인터페이스는 객체를 생성하지 않기 때문에 인스턴스 멤버 변수와 생성자가 없으며, 클래스 멤버 변수만 생성할 수 있다.

멤버 변수는 항상 public static final 이며, 해당 키워드는 생략 가능하다.

인터페이스의 멤버 메소드

인터페이스는 오버라이딩을 위한 메소드이기 때문에 public 이어야하며, 선언된 모든 메소드는 abstract 메소드이다.

따라서 멤버 메소드는 항상 public abstract 이며, 이 키워드는 생략 가능하다.

이때, static 메소드는 구현부가 있어야 하고 오버라이딩이 불가능하므로 인터페이스의 메소드가 될 수 없다. 마찬가지로 final 메소드도 오버라이딩이 불가능하기 때문에 인터페이스의 메소드가 될 수 없다.

하지만, 자바 버전이 올라가면서 static 메소드는 사용할 수 있게 되었다. (아래 '질문' 참고)

Abstract 클래스

abstract 클래스는 자바에서 마음대로 초기화하고 실행할 수 없도록 되어 있다. 그래서 abstract 클래스를 구현해 놓은 클래스로 초기화 및 실행해야 한다.

선언

abstract 클래스는 class 예약어 앞에 abstract 를 붙여 사용한다. 그리고 몸통이 없는 메소드 선언문에서도 abstract 를 명시해준다.

public abstract class 클래스이름 {
	public abstract 리턴타입 메소드이름();
	...
}

abstract 클래스는 abstract 로 선언한 메소드가 하나라도 있을 때 선언한다. 그리고 인터페이스와 달리 구현되어 있는 메소드가 있어도 상관 없다.

정리하자면,

  • abstract 클래스는 클래스 선언 시 abstract 라는 예약어가 클래스 앞에 추가되면 된다.
  • abstract 클래스 안에는 abstract 으로 선언된 메소드가 0개 이상 있으면 된다.
  • abstract 로 선언된 메소드가 하나라도 있으면, 그 클래스는 반드시 abstract 로 선언되어야만 한다.
  • abstract 클래스는 몸통이 있는 메소드가 0개 이상 있어도 상관없으며, static이나 final 메소드가 있어도 된다.

구현

abstract 클래스는 이름에서 알 수 있듯 클래스이기 때문에 extends 예약어를 사용해야 한다.

그리고 이렇게 상속받은 클래스에서는 abstract 클래스에서 abstract 로 선언되어 있는 메소드들을 구현해야 한다.

public class Impl extends abstract클래스 {
}

💡 extends 를 사용하므로, 다중 상속이 불가능함을 알 수 있다.

abstract 클래스를 사용하는 이유

인터페이스를 선언하다 보면 어떤 메소드는 미리 만들어 놓아도 전혀 문제가 없는 경우가 발생한다. 그렇다고 따로 해당 클래스를 만들기는 애매하기도 하다.

특히 아주 공통적인 기능을 미리 구현해 놓으면 많은 도움이 된다.

이런 경우에 abstract 클래스를 사용한다.

인터페이스 vs abstract 클래스 vs 클래스

인터페이스abstract 클래스클래스
선언 시 사용하는 예약어interfaceabstract classclass
구현 안 된 메소드 포함 가능 여부가능(필수)가능불가
구현된 메소드 포함 여부불가가능가능(필수)
static 메소든 포함 가능 여부불가가능가능
final 메소드 선언 가능 여부불가불가가능
상속(extends) 가능불가불가가능
구현(implements) 가능가능가능불가

final 예약어

이 예약어는 클래스, 메소드, 변수에 선언할 수 있다.

클래스에 선언 시

클래스가 final로 선언되어 있으면 상속할 수 없다.

public final class 클래스이름 {}

이렇게 사용하는 대표적인 클래스로는 String 클래스가 있다. 이 클래스는 개발자가 조금이라도 변경해서 작업하면 안된다. 만약 toString()을 메소드를 수정해버린다면 String이라는 클래스에 대한 기본 속성을 변경하는 것이 된다.

이처럼 더 이상 확장해서는 안되는 클래스, 누군가 이 클래스를 상속받아서 내용을 변경하면 안 되는 클래스를 선언할 때 final로 선언한다.

메소드에 선언 시

메소드를 final로 선언하면 더 이상 Overriding할 수 없다.

public final void 메소드이름() {}

클래스와 같은 이유로, 누군가 메소드를 변경하지 못하도록 하려할 때 사용한다. 실제로는 메소드에는 많이 사용되지 않는다.

변수에 선언 시

변수에 final을 사용하면 그 변수는 더 이상 바꿀 수 없다.

따라서 인스턴스 변수나 static으로 선언된 클래스 변수는 선언과 함께 값을 지정해야만 한다.

final int variable = 1;

인스턴스 변수와 클래스 변수 초기화 시점

인스턴스 변수와 클래스 변수는 변수 생성과 동시에 초기화를 해야만 컴파일 시 에러가 발생하지 않는다.

생성자나 메소드에서 초기화 하는 것은 중복되어 변수값이 선언될 수 있어 final의 기본 의도를 벗어나므로 허용되지 않는다.

매개 변수와 지역 변수 초기화 시점

반면, 매개 변수나 지역 변수를 final로 선언하는 경우에는 반드시 선언할 때 초기화할 필요는 없다.

매개 변수는 이미 초기화를 되어서 넘어 왔고, 지역 변수는 메소드를 선언하는 중괄호 내에서만 참조되므로 다른 곳에서 변경할 일이 없기 때문이다.

다만, 둘 다 아래와 같이 재선언은 안된다.

public void method(final int parameter) {
	final int local;
	local = 2;       // 문제 없음
	local = 3;       // ERROR
	parameter = 4;   // ERROR
}

final을 사용하는 이유

달력처럼 “변하지 않는 값”에 final을 선언하여 사용하면, 누구나 이 변수를 가져다가 쓸 수 있다.

그래도 이 예약어는 남발해서는 안됨을 유의하자.

final 참조 자료형

앞에서 설명한 것은 기본 자료형에 사용한 final의 경우이다. 그렇다면, 참조 자료형은 어떻게 적용될까?

기본 자료형과 마찬가지로 참조 자료형도 두 번 이상 값을 할당하거나 새로 생성자를 사용하여 초기화할 수 없다.

public class FinalReferenceType {
    final MemberDTO dto=new MemberDTO();
    public static void main(String args[]) {
        FinalReferenceType referenceType=new FinalReferenceType();
        referenceType.checkDTO();
    }
    public void checkDTO() {
        System.out.println(dto);
        dto=new MemberDTO();     // ERROR
    }
}

MemberDTO 타입의 변수를 final로 선언하고, 인스턴스 변수이므로 선언과 동시에 초기화를 해주었다. 그리고 checkDTO() 에서 객체를 다시 생성해 넣어봤다.

위 코드를 컴파일하면 아래와 같은 오류가 발생한다.

하지만, 아래처럼 코드를 변경하면 컴파일 및 실행 결과 값이 변하는 것을 확인할 수 있다.

public class FinalReferenceType {
    final MemberDTO dto=new MemberDTO();
    public static void main(String args[]) {
        FinalReferenceType referenceType=new FinalReferenceType();
        referenceType.checkDTO();
    }
    public void checkDTO() {
        System.out.println(dto);
        // dto=new MemberDTO();
        dto.name="Sangmin";
        System.out.println(dto);
    }
}

MemberDTO 클래스의 객체는 두 번 이상 생성할 수 없다. 하지만, 그 객체의 안에 있는 객체들은 final로 선언된 것이 아니기 때문에 그러한 제약이 없다.

즉, 해당 클래스가 final이라고 해서, 그 안에 있는 인스턴스 변수나 클래스 변수는 final은 아니다.

enum 클래스

enum은 enumeration이라는 “셈, 계산, 열거, 목록, 일람표”라는 영어 단어의 앞부분을 따서 만들어진 예약어이다. JDK 1.5에서 처음 소개되었다.

enum 클래스는 어떻게 보면 타입이지만, 클래스의 일종이다. 열거형 클래스라고도 부른다.

fianl로 String과 같은 문자열이나 숫자들을 나타내는 기본 자료형의 값을 고정할 수 있다. 이렇게 고정된 값을 “상수”라고 하고, 영어로는 Constant라고 한다.

그런데, 어떤 클래스가 상수로만 만들어져 있을 경우에는 반드시 class로 선언할 필요는 없다.

이 때 사용하는 것이 enum 클래스로, “이 객체는 상수의 집합이다.”라는 것을 명시적으로 나타낸다.

선언

  • class 라고 선언하는 부분에 enum 이라고 선언한다.
  • 해당 상수들의 이름을 콤마로 구분하여 나열한다.
public enum OverTimeValues {
    THREE_HOUR,
    FIVE_HOUR,
    WEEKEND_FOUR_HOUR,
    WEEKEND_EIGHT_HOUR;
}

위 예제처럼 enum 클래스에 있는 상수들은 지금까지 살펴본 변수들과 다르게 별도로 타입을 지정할 필요도, 값을 지정할 필요도 없다.

switch 문에서 enum 사용

가장 효과적으로 enum 클래스를 사용하는 방법은 switch 문에서 사용하는 것이다.

예제

enum 타입을 매개 변수로 받고, swtich 조건으로 해당 매개 변수를 지정한다. 그리고 case에는 enum에서 선언한 상수 값들로 분기하도록 사용한다.

public class OverTimeManager {
    public int getOverTimeAmount(OverTimeValues value) {
        int amount=0;
        System.out.println(value);
        switch(value) {
            case THREE_HOUR:
                amount=18000;
                break;
            case FIVE_HOUR:
                amount=30000;
                break;
            case WEEKEND_FOUR_HOUR:
                amount=40000;
                break;
            case WEEKEND_EIGHT_HOUR:
                amount=60000;
                break;
        }
        return amount;
    }
}

위 함수를 호출할 때 “enum 클래스 이름.상수 이름”을 매개 변수로 넘겨준다. enum 타입은 “enum 클래스 이름.상수 이름”을 지정함으로써 클래스의 객체 생성이 완료된다고 생각하면 된다.

public static void main(String args[]) {
	OverTimeManager manager=new OverTimeManager();
	int myAmount=manager.getOverTimeAmount(OverTimeValues.THREE_HOUR);
	System.out.println(myAmount);
}

💡 콤마로 구분되어 선언된 상수들은 객체라고 생각하면 쉽다.

그리고 다음과 같은 코드를,

int myAmount=manager.getOverTimeAmount(OverTimeValues.THREE_HOUR);

아래처럼 풀어써서 구현할 수 있다.

OverTimeValues value = OverTimeValues.THREE_HOUR;
int myAmount=manager.getOverTimeAmount(value);

여기서 value 는 enum 클래스의 객체라고 생각하면 된다.

enum 상수에 값 지정

enum 상수 값을 지정하는 것은 가능하다. 단, 값을 동적으로 할당하는 것은 불가능하다.

  • package-private(아무것도 쓰지 않은 상태)와 private만 접근 제어자로 생성자를 만든다.
  • 상수(생성자의 매개 변수) 로 선언한다.
public enum OverTimeValues2 {
    THREE_HOUR(18000),
    FIVE_HOUR(30000),
    WEEKEND_FOUR_HOUR(40000),
    WEEKEND_EIGHT_HOUR(60000);
    private final int amount;
    OverTimeValues2(int amount) {
        this.amount=amount;
    }
    public int getAmount(){
        return amount;
    }
}

이렇게 값을 할당하면, 다음과 같이 쓸 수 있다.

OverTimeValues2 value2=OverTimeValues2.FIVE_HOUR;
System.out.println(value2);              // FIVE_HOUR
System.out.println(value2.getAmount());  // 30000

장단점

이처럼 상수 값을 지정했을 경우, 지정하지 않았을 경우에 비해 성능은 훨씬 좋다.

하지만, enum 클래스의 상수는 변하지 않는 값이므로, 안의 상수 값을 변경하고 싶을 경우 자바 프로그램을 수정한 후 다시 컴파일해서 실행 중인 자바 프로그램을 중지했다가 다시 시작해야 한다는 단점이 존재한다.

주의할 점

public이나 protected를 생성자로 사용해서는 안된다. 다시 말해, 각 상수를 enum 클래스 내에서 선언할 때에만 이 생성자를 사용할 수 있다.

따라서 enum 클래스는 생성자를 통하여 객체를 생성할 수는 없다.

💡 package-private의 실제 사용 여부
책에서는 package-private은 사용할 수 있다고 했다. 따라서 “패키지 내에서 enum 클래스의 생성자를 호출할 수 있지 않을까?” 하는 생각을 했다.

하지만 실제로는 package-private처럼 아무것도 쓰지 않았을 경우에는 java에서 자동으로 private으로 선언해 준다고 한다. 따라서 enum 클래스의 생성자는 private만 사용할 수 있다.

enum 클래스의 부모, java.lang.Enum

enum 클래스는 무조건 java.lang.Enum 이라는 클래스의 상속을 받는다.

우리가 enum 클래스를 선언할 때 extends java.lang.Enum 를 사용하지 않아도, 컴파일러가 알아서 이 문장을 추가해서 컴파일 한다.

따라서, enum을 선언하여 사용할 때 마음대로 extends하면 안 된다. 그리고 누군가 만들어 놓은 enum을 extends를 이용해 선언할 수 없다.

Enum 클래스의 생성자

접근 제어자메소드설명
protectedEnum(String name, int ordinal)컴파일러에서 자동으로 호출되도록 해놓은 생성자다. 하지만, 개발자가 이 생성자를 호출할 수는 없다.

여기서 name은 enum 상수의 이름이고, ordinal은 enum의 순서이며 상수가 선언된 순서대로 부터 증가한다.

Overriding이 금지된 메소드

Enum 클래스의 부모 클래스는 Object 클래스이기 때문에, Object 클래스의 메소드들은 모두 사용할 수 있다.

하지만, Enum 클래스는 Object 클래스 중 아래 4개의 메소드는 Overriding하지 못하도록 막아놓았다.

메소드내용
clone()객체를 복사하기 위한 메소드. enum 클래스에서 호출 시 CloneNotSupportedException 예외를 발생시킨다.
finalize()GC가 발생할 때 처리하기 위한 메소드다.
hashCode()int 타입의 해시 코드 값을 리턴하는 메소드다.
equals()두 개의 객체가 동일한지를 확인하는 메소드다.

💡 책에서는 이 네 가지 중 finalize()와 clone() 메소드는 사용하면 안된다고 한다.

enum.toString()

enum 변수에 이 메소드를 호출하면 앞에서 살펴본 것처럼 상수 이름을 출력한다.

toString() 메소드는 Enum 클래스에서 Overriding한 Object 클래스의 메소드 중에서 유일하게 final로 선언되어 있지 않다. 그러므로, Overriding해도 전혀 상관없다.

Enum 클래스의 메소드

메소드내용
compareTo(E e)매개 변수로 enum 타입과의 순서(ordinal) 차이를 리턴한다.
getDeclaringClass()클래스 타입의 enum을 리턴한다.
name()상수의 이름을 리턴한다.
ordinal()상수의 순서를 리턴한다.
valueOf(Class<T> enumType, String name)static 메소드다. 첫 번째 매개 변수로는 클래스 타입의 enum을, 두 번째 매개 변수로는 상수의 이름을 넘긴다.

이 중에서는 compareTo() 메소드를 많이 사용한다.

Enum은 선언된 순서대로 각 상수들의 숫자가 정해지는데, compareTo 메소드는 같은 상수라면 0을, 그렇지 않다면 순서의 차이를 출력한다.

순서의 차이는 매개 변수로 넘어온 상수 기준으로 앞에 있으면 음수(-)를, 뒤에 있으면 양수(+)를 리턴한다.

public class OverTimeManager2 {
    public static void main(String args[]) {
        OverTimeValues2 value2=OverTimeValues2.FIVE_HOUR;
        OverTimeValues2 value3=OverTimeValues2.THREE_HOUR;
        System.out.println(value2.compareTo(value3)); // 1을 반환
    }
}

API 문서에 없는 특수한 메소드, values()

values() 메소드는 enum 클래스에는 API 문서에 없는 특수한 메소드로, static으로 선언되어 있어 바로 호출할 수 있다.

이 메소드를 호출하면 enum 클래스에 선언되어 있는 모든 상수를 배열로 리턴한다. 어떤 상수가 어떤 순서로 선언되었는지 확인하기 어려운 경우에 이 메소드를 사용하면 많은 도움이 될 것이다.

아래와 같이 for문을 사용하여 확인할 수 있다.

public class OverTimeManager3 {
    public static void main(String args[]) {
        OverTimeValues2 []valueList=OverTimeValues2.values();
        for(OverTimeValues2 value:valueList) {
            System.out.println(value);
        }
    }
}

요약

인터페이스와 abstract 클래스는 클래스의 골격을 잡아주고, 메소드를 선언해 놓을 때 매우 유용하게 사용할 수 있다.

enum이라는 열거형 클래스는 이름대로 열거되어 있는 데이터나 상수를 처리할 때 사용하므로, 고정되어 있는 값을 처리할 때 사용하면 많은 도움이 될 것이다.

정리해 봅시다.

Q. 인터페이스에 선언되어 있는 메소드는 body(몸통)이 있어도 되나요?

Me: X

Q. 인터페이스를 구현하는 클래스의 선언시 사용하는 예약어는 무엇인가요?

Me: implements

Q. 메소드의 일부만 완성되어 있는 클래스를 무엇이라고 하나요?

Me: abstract 클래스

Q. 위에 있는 문제의 답에 있는 클래스에 body(몸통)이 없는 메소드를 추가하려면 어떤 예약어를 추가해야 하나요?

Me: abstract

Q. 클래스를 final로 선언하면 어떤 제약이 발생하나요?

Me: 상속(확장)할 수 없다.

Q. 메소드를 final로 선언하면 어떤 제약이 발생하나요?

Me: override 할 수 없다.

Q. 변수를 final로 선언하면 어떤 제약이 발생하나요?

Me: 값을 변경할 수 없다.

Q. enum 클래스 안에 정의하는 여러 개의 상수들을 나열하기 위해서 상수 사이에 사용하는 기호는 무엇인가요?

Me: 콤마(,)

Q. enum 으로 선언한 클래스는 어떤 클래스의 상속을 자동으로 받게 되나요?

Me: java.lang.Enum

Q. enum 클래스에 선언되어 있지는 않지만 컴파일시 자동으로 추가되는. 상수의 목록을 배열로 리턴하는 메소드는 무엇인가요?

Me: values()

질문

💡 책에 있는 내용이 아닙니다.

책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.

Q. 인터페이스 내부에 추상 메소드가 아닌 메소드를 넣을 수 있을까?

본래 추상 메소드가 아닌 메소드를 넣을 수 없었지만, java8 에서 디폴트 메소드와 스태틱 메소드를 사용할 수 있게 되었다.

Java 8 이후에 함수형 프로그래밍과 관련된 람다식과 스트림 API를 도입함에 따라 필요한 유연성과 표현력이 필요해졌다.

기존 인터페이스의 문제점

인터페이스에는 이런 문제가 있었다.

  • 호환성 문제
    • 기존의 인터페이스를 수정하면 해당 인터페이스를 구현하는 모든 클래스에서 해당 메소드를 수정해야 한다.
    • 이는 기존 코드들과의 호환성을 해칠 수 있다.
  • 확장성 문제
    • 기존의 인터페이스에 새로운 메소드를 추가하면 해당 인터페이스를 구현하는 모든 클래스에서 새로운 메소드를 구현해야 한다.
    • 이는 모든 클래스를 변경해야 하는 번거로움이 발생한다.

람다식과 스트림 API의 추가

java8 버전부터 함수형 프로그래밍과 관련된 람다식과 스트림 API가 추가되었다.

이 추가된 기능을 컬렉션(Collection) 클래스에서 사용하기 위해, 기존에 만들어 놓았던 인터페이스들을 구현하고 있는 컬렉션 클래스들의 구조에서 특정한 기능을 추가해야 되는 상황이 오게 되었다.

이 때 위의 두 가지 문제가 발생하게 된다. 기존 인터페이스에 추상 메소드를 추가하거나 수정하면, 인터페이스를 구현하고 있는 모든 구현 클래스도 변경해줘야 하는 것이다.

따라서 디폴트 메소드와 스태틱 메소드가 추가하여 호환성과 확장성을 개선했다.

static 메소드

public interface MyInterface {
    static void staticMethod() {
        System.out.println("This is a static method.");
    }
}
  • 일반 static 메소드와 똑같이 사용한다.
  • static 메소드는 상속 불가능하다.
  • static 메소드는 인터페이스를 구현하지 않아도 사용할 수 있다.
    • MyInterface.staticMethod();

기존의 static 메소드 위치

static 메소드가 허용되지 않았을 때에는, 인터페이스와 관련된 static 메소드는 별도의 클래스에 따로 두어야 했다.

대표적으로 java.util.Collection 인터페이스가 있다. 이 인터페이스와 관련된 static 메소드들은 별도의 클래스인 Collections 라는 클래스에 들어가 있다.

그렇지만 자바8에 와서 위의 제약은 없어지게 되었다.

default 메소드

public interface MyInterface {
    // 디폴트 메소드
    default void defaultMethod() {
        System.out.println("This is a default method.");
    }
}
  • 접근 제어자는 public 이며 생략 가능하다.
  • 리턴타입 앞에 default 키워드를 붙이며, 구현부가 있어야 한다.
  • 자식 클래스에서 default 메소드를 오버라이딩할 수 있다.
  • 인터페이스명.super.디폴트메서드 로 원래의 디폴트 메서드 호출이 가능하다.

사용 이유

기존 코드의 하위 호환성을 유지하면서 새로운 기능을 추가하기 위해 사용한다.

예를 들어, 인터페이스를 구현하는 모든 구현체에게 함수를 만들어주고 싶을 때 사용된다.

주의할 점

인터페이스는 Object 클래스를 상속 받지 않기 때문에, Object 클래스가 제공하는 기능(equals, hasCode)는 기본 메소드로 제공할 수 없다.

따라서 구현체가 직접 재정의를 해 주어야 한다.

default 메소드 다중 상속 문제

구현부가 있는 디폴트 메소드는 인터페이스를 다중 구현할때 클래스 다중 상속 문제와 똑같은 문제를 발생시킨다.

따라서 인터페이스 다중 구현에 한해서 자바에서는 다음과 같은 규칙을 정하였다.

  1. 다중 인터페이스들 간의 디폴트 메소드 충돌
    • 똑같은 디폴트 메소드를 가진 두 인터페이스를 하나의 클래스에 구현하고 아무런 조치를 취하지 않으면 컴파일 자체가 되지 않는다
    • 인터페이스를 구현한 클래스에서 디폴트 메소드를 오버라이딩하여 하나로 통합한다.
  2. 인터페이스의 디폴트 메소드와 부모 클래스 메소드 간의 충돌
    : 인터페이스와 부모 클래스를 동시에 extends / implements 했을 경우
    • 부모 클래스의 메소드가 상속되고 디폴트 메소드는 무시한다.
    • 만일 인터페이스 쪽의 디폴트 메소드를 사용할 필요가 있다면, 상속받은 메소드를 디폴트 메소드와 같은 내용으로 오버라이딩하여 사용해야 한다.

private 메소드

자바9 버전에 추가된 메소드이다.

인터페이스에 default, static 메소드가 생긴 이후, 이러한 메소드들의 로직을 공통화하고 재사용하기 위해 생긴 메소드라고 한다.

  • 인터페이스 내부에서만 돌아가는 코드이다.
  • 인터페이스를 구현한 클래스에서 사용하거나 오버라이딩 할 수 없다.
  • private 메소드를 호출할 때는 default 메소드 내부에서 호출한다.
  • private static 메소드는 static 메소드에서만 호출이 가능하다.

사용 예제 (feat. ChatGPT)

public interface MyInterface {
    default void publicMethod() {
        int result = sharedLogic(5);
        System.out.println("Result: " + result);
    }

    default void anotherMethod() {
        int result = sharedLogic(10);
        System.out.println("Result: " + result);
    }

    private int sharedLogic(int value) {
        // 공통적으로 사용되는 로직
        return value * 2;
    }
}

Q. interface 내부에서 static 블록 사용 여부

사용하지 못한다.

Q. 다중 인터페이스를 구현할 때, 같은 이름의 메소드가 있을 경우

아무런 문제도 되지 않는다. 왜냐하면 인터페이스에는 구현부가 없기 때문이다.

Q. 인터페이스 내부에 변수 사용

인터페이스 멤버 변수는  public static final 로만 지정 가능하며 생략 가능하다. 또한 멤버 함수는 public abstract final 로만 선언 가능하다.

실제 변수를 int value = 0; 으로 선언하는 경우  public static final가 생략되어 있는 것이다.

public static final int value = 100;
public String str = "s"; //static final 생략 가능 
public abstract final void print(){ }
public void draw(){ }

Q. 인터페이스의 접근제어자

자바에서 interface 에 정의되는 메소드는 외부로 공개되는 메소드를 정의하는 것이다. 그러므로 private 나 protected 로 정의가 불가능하다.

public interface MainService {
	
	public void test() ;
	package void test2();                  // ERROR
	private static abstract void test3();  // ERROR
}

Illegal combination of modifiers for the private interface method test3; additionally only one of static and strictfp is permitted

Q. 내부가 빈 인터페이스를 생성하면 컴파일 및 실행이 될까?

물론 된다. 찾아보니까, 이 클래스는 무슨 타입이다를 명시하고 싶을 때 빈 인터페이스를 만들어 구현시키기도 한다고 한다.

Q. final class의 변수 호출 및 변경

상속(확장) 불가이기 때문에, 객체로 생성하여 사용하는 것은 상관없다.

public final class Test {
    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println(t1.getName());

        Test t2 = new Test("Min");
        System.out.println(t2.getName());
    }
    
    private String name = "";
    public Test() {
        this.name = "null";
    }
    public Test(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

Q. 지역 변수를 final로 사용할 경우, 초기화가 필수이지 않나?

아래와 같이 초기화하지 않고 사용하면 문제가 생긴다.

하지만, 선언 후 초기화한 후 사용한다면 문제가 되지 않는다.

이는 일반 변수도 마찬가지.

Q. enum은 생성자도 없고, static 예약어도 없는데 어떻게 바로 사용할 수 있을까?

사실 enum은 static class이다. 이를 지정하지 않았을 경우에도, 자바 프로그램에서 자동으로 static으로 인식하기 때문에 바로 사용할 수 있는 것이다.

Q. Enum의 기본 생성자에 매개변수는 어떻게 넘겨주는가?

enum 은 enum 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.

public enum enum이름 {
	상수(초기화값);
}

위 코드는, 사실 아래와 같은 것이다.

public static final enum이름 상수이름 = new enum이름(초기화값);

Q. enum.상수 는 어떤 값이 출력되는가?

매핑된 열거 데이터(상수 값)이 아닌 열거 객체명이 출력된다.

Enum 타입 객체도 하나의 데이터 타입이므로 변수를 선언하고 사용하면 된다.

한 가지 알아둘 점은 enum 타입은 특수한 클래스 라는 점이다.

즉, primitive 타입이 아닌 referece 타입으로 분류되며, 그래서 enum 상수 값은 힙(heap) 영역에 저장되게 된다.

String 처럼 스택 영역에 있는 변수들이 힙 영역에 있는 데이터의 주소값을 저장함으로써 참조 형태를 띄게 된다. 그래서 다음과 같이 같은 enum 타입 변수 끼리 같은 상수 데이터를 바라봄으로써 둘이 주소를 비교하는 ==연산 결과는 true가 되게 된다.

마찬가지로 enum 상수들을 배열로 만들어 저장할 시에도, 각 배열 원소들 마다 참조 주소값들이 저장되어 힙 영역의 상수 데이터들을 가리키게 된다.

Q. enum 생성자의 접근 제어자

enum은 열거형 상수이기 때문에, 값이 변경되면 안된다. 따라서 public일 수 없다. 찾아보면 enum은 나만 호출할 수 있어야 하므로 private이어야만 한다고 한다.

또한, 아무것도 선언하지 않은 상태인 package-private은 컴파일러가 자동으로 private을 붙여주기 때문에 사실은 private으로만 사용하는 거라고 할 수 있다.

Q. Enum에서 왜 Object의 4개의 메소드를 오버라이딩 못하도록 했는가?

enum 은 고유한 상수(public static final)이기 때문에 오버라이딩해서 자기 마음대로 바꿔버리면 고유성이 깨지기 때문에, 메소드를 final화 하여 막아놓았다고 한다.

Q. 상속은 언제 쓰고, 인터페이스는 언제 쓸까?

아래는 내 생각을 정리해 적은 것이다.

상속은 부모 클래스의 속성과 행위가 자식 클래스도 갖고 있음을 말하는 것이다. 즉, 공통된 속성과 기능을 하나로 빼두어 코드 중복을 줄이고 재사용할 수 있을 때 사용한다.

그리고 인터페이스는 추상화가 목적이다. 선언과 구현을 분리하여, 내부 구현을 숨기고 여러 타입을 하나의 타입으로 받을 수 있는 다형성을 제공한다. 또한, 상속과 다르게 다중 상속이 가능하다는 점도 있다.

요약

상속

  • 일반화 관계(is-a)일 경우
  • 부모 클래스의 코드를 재사용하고 확장할 경우

인터페이스

  • 다중 상속이 필요한 경우
  • 클래스가 따라야 할 규약을 정의할 경우
  • 선언과 구현을 분리하여 다형성을 구현할 경우

참고 사이트

☕ 인터페이스(Interface) 문법 & 활용 - 완벽 가이드

☕ 자바 Enum 열거형 타입 문법 & 응용 💯 정리

Java: enum의 뿌리를 찾아서...

Chapter 8. Classes

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글