Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

랩터

[Java] 추상화 본문

공부/JAVA

[Java] 추상화

raptorhs 2024. 6. 29. 18:03

추상화

먼저 “추상”이라는 용어의 사전적 의미를 보면 “사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것"이라고 정의합니다. 자바에서의 추상화는 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미합니다.

 

상속이 하위 클래스를 정의하는데 상위 클래스를 사용하는 것이라고 한다면 추상화는 반대로 기존 클래스들의 공통적인 요소들을 뽑아서 상위 클래스를 만들어 내는 것이라고 할 수 있습니다.

 

자바에서는 주로 추상클래스와 인터페이스라는 문법 요소를 사용해서 추상화를 구현함.

 

abstract  제어자

abstract는 주로 클래스와 메서드를 형용하느 키워드로 사용, 메서드 앞에 붙은 경우를 '추상 메서드', 클래스 앞에 붙은 경우를 '추상 클래스'라 각각 부릅니다.

어떤 클래스에 추상 메서드가 포함되어 있는 경우 해당 클래스는 자동으로 추상 클래스가 됩니다.

abstract class AbstractExample { // 추상메서드가 최소 하나 이상 포함돼있는 추상 클래스
	abstract void start(); // 메서드 바디가 없는 추상메서드
}

추상적이다 = "충분히 구체적이지 않다"

추상메서드는 '미완성 메서드'이며, 미완성 메서드를 포함하는 클래스는 '미완성 클래스'를 의미하는 추상 클래스가 됩니다.

추상 클래스는 미완성 설계도이기 때문에 메서드 바디가 완성되기 전까지 이를 기반으로 객체 생성이 불가합니다.

 

추상클래스

객체도 생성하지 못하는 미완성 클래스를 왜 만드는 걸까?

먼저 추상 클래스는 상속 관계에 있어 새로운 클래스를 작성하는 데 매우 유용합니다. 

abstract class Animal{
    public String kind;
    public abstract void sound();
}
class Dog extends Animal{ // Animal 클래스로부터 상속
    public Dog(){
        this.kind = "포유류";
    }
    public void sound(){//메서드 오버라이딩 -> 구현부 완성
        System.out.println("멍멍");
    }
}
class Cat extends Animal{
    public Cat(){
        this.kind = "포유류";
    }

    public void sound(){// 메서드 오버라이딩 -> 구현부 완성
        System.out.println("야옹");
    }
}

 class DogExample {
     public static void main(String[] args) throws Exception{
         Animal dog = new Dog();
         dog.sound();

         Cat cat = new Cat();
         cat.sound();
     }
}

메서드의 내용이 상속을 받는 클래스에 따라서 종종 달라지기 때문에 상위 클래스에서는 선언부만을 작성하고, 실제 구체적인 내용은 상속을 받는 하위 클래스에서 구현하도록 비워둔다면 설계하는 상황이 변하더라도 보다 유연하게 변화에 대응할 수 있습니다. 

이때 우리가 사용하게 되는 것이 앞서 상속 파트에서 학습한 ‘오버라이딩'입니다.

추상 클래스를 사용하면 상속을 받는 하위 클래스에서 오버라이딩을 통해 각각 상황에 맞는 메서드 구현이 가능하다는 장점이 있습니다.

 

두 번째로, 추상 클래스는 자바 객체지향 프로그래밍의 마지막 기둥인 추상화를 구현하는데 핵심적인 역할을 수행합니다.

추상화를 한마디로 정리하면 “객체의 공통적인 속성과 기능을 추출하여 정의하는 것”이라 정리할 수 있습니다.

앞선 예시를 다시 보면, 동물이 가지는 공통적인 특성을 모아 먼저 추상 클래스로 선언해 주었고, 이를 기반으로 각각의 상속된 하위 클래스에서 오버라이딩을 통해 클래스의 구체적인 내용을 결정해 주었습니다.

 

결론적으로 구체화에 반대되는 개념으로 추상화를 생각해 보면, 상속계층도의 상층부에 위치할수록 추상화의 정도가 높고 그 아래로 내려갈수록 구체화된다고 정리해 볼 수 있습니다.

다른 말로, 상층부에 가까울수록 더 공통적인 속성과 기능들이 정의되어 있다고 생각할 수 있습니다.

 

final 키워드

영어로 ‘최종의', ‘마지막의'라는 뜻을 가지고 있는 final 키워드는 필드, 지역 변수, 클래스 앞에 위치할 수 있으며 그 위치에 따라 그 의미가 조금씩 달라지게 됩니다.

 

위치                           의미

클래스 변경 또는 확장 불가능한 클래스, 상속 불가
메서드 오버라이딩 불가
변수 값 변경이 불가한 상수

 

 

final class FinalEx { // 확장 상속불가능한 클래스
    final int x = 1;//변경되지 않는 상수

    final int getNum() { // 오버라이딩 불가한 메서드
        final int localVar = x; // 상수
        return x;
    }
}

 

final 제어자가 추가되면 이제 해당 대상은 더 이상 변경이 불가하거나 확장되지 않는 성질을 지니게 됩니다.

 

인터페이스

인터페이스

인터페이스가 무엇이며, 어떻게 사용될 수 있는지 살펴보자.

기본적으로 인터페이스도 추상 클래스처럼 자바에서 추상화를 구현하는 데 활용된다는 점에서 동일하지만, 추상 클래스에 비해 더 높은 추상성을 가진다는 점에서 큰 차이가 있습니다.

 

추상클래스는 미완성 설계도지만 인터페이스는 그보다 더 높은 추상성을 가지는 가장 기초적인 
"밑그림"에 빗대어 표현할 수 있습니다.

 

인터페이스는 기본적으로 추상 메서드와 상수만을 멤버로 가질수 있다. 인터페이스는 기본적으로  "추상 메서드의 집합"으로 이뤄져 있다.

 

인터페이스의 기본 구조

인터페이스를 작성하는 것은 기본적으로 클래스를 작성하는 것과 유사하지만, class 키워드 대신 interface 키워드를 사용한다는 점에서 차이가 있습니다.

일반 클래스와 다르게, 내부의 모든 필드가 public static final로 정의되고, static과 default메서드 이외의 모든 메서드가 public abstract로 정의된다는 차이가 존재합니다.

 

public interface InterfaceEx {
    public static final int rock = 1; // 인터페이스 인스턴스 변수 정의
    final int scissors = 2; // public static 생략
    static int paper = 3; // public & final 생략
    
    public abstract String getPlayingNum();
    void call(); // public abstract 생략
}

 

인터페이스 안에서 상수를 정의하는 경우에는 반드시 public static final로, 메서드를 정의하는 경우에는 public abstract로 정의되어야 하지만 위에 있는 코드처럼 일부분 또는 전부 생략이 가능합니다. 생략된 부분은 컴파일러가 자동으로 추가해주게 됩니다.

 

인터페이스의 구현

추상 클래스와 마찬가지로 인터페이스도 그 자체로 인스턴스를 생성할 수 없고, 메서드 바디를 정의하는 클래스를 따로 작성해야 합니다. extends키워드 대신 구현하다라는 의미를 가진 implements키워드를 사용합니다.

class 클래스명 implements 인터페이스명 {
		... // 인터페이스에 정의된 모든 추상메서드 구현
}

 

특정 인터페이스를 구현한 클래스는 해당 인터페이스에 정의된 모든 추상메서드를 구현해야 합니다.

= 그 클래스에게 인터페이스의 추상 메서드를 반드시 구현하도록 강제함.

= 그 인터페이스가 가진 모든 추상 메서드들을 해당 클래스내에서 오버라이딩하여 바디를 완성한다는 뜻.

 

 

인터페이스의 다중 구현

앞서 배웠던 것처럼, 클래스 간의 상속에서 다중 상속은 허용되지 않습니다.

즉 하위 클래스는 단 하나의 상위 클래스만 상속받을 수 있습니다.

반면 인터페이스는 다중적 구현이 가능합니다.

다시 말해, 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있습니다. 다만 인터페이스는 인터페이스로부터만 상속이 가능하고, 클래스와 달리 Object 클래스와 같은 최고 조상이 존재하지 않습니다.

 

 interface Animal {
    public abstract void cry();
}
interface Pet{
    void play();
}
class Dog implements Animal, Pet{ // Animal과 Pet인터페이스 다중 구현
    public void cry(){//메서드 오버라이딩
        System.out.println("멍멍!");
    }
    public void play(){ // 메서드 오버라이딩
        System.out.println("원반 던지기");
    }
}
class Cat implements Animal, Pet {
    public void cry(){
        System.out.println("야옹~!");
    }
    public void play(){
        System.out.println("쥐 잡기");
    }
}
public class MultiInheritance {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.cry();
        dog.play();
        cat.cry();
        cat.play();
    }
}

 

DogCat 클래스는 각각 AnimalPet 인터페이스를 다중으로 구현하여 각각의 객체에 맞는 메서드를 오버라이딩하고 그 내용을 출력값으로 돌려주고 있습니다. 인터페이스는 애초에 미완성된 멤버를 가지고 있기 때문에 충돌이 발생할 여지가 없고, 따라서 안전하게 다중 구현이 가능합니다.

 

특정 클래스는 다른 클래스로부터의 상속을 받으면서 동시에 인터페이스를 구현할 수 있습니다.

 abstract class Animal { // 추상클래스
    public abstract void cry();
}
interface Pet{
    public abstract void play();
}
class Dog extends Animal implements Pet{ // Animal클래스 상속 & Pet 인터페이스 구현
    public void cry(){//메서드 오버라이딩
        System.out.println("멍멍!");
    }
    public void play(){ // 메서드 오버라이딩
        System.out.println("원반 던지기");
    }
}
class Cat extends Animal implements Pet {
    public void cry(){
        System.out.println("야옹~!");
    }
    public void play(){
        System.out.println("쥐 잡기");
    }
}
public class MultiInheritance {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();

        dog.cry();
        dog.play();
        cat.cry();
        cat.play();
    }
}

 

인터페이스의 장점

Provider 클래스에 의존하고 있는 User 클래스가 있습니다.

여기서 “의존한다"라는 말의 의미가 조금 어렵게 느껴질 수 있는데, 쉽게 표현하면 User 클래스에서 Provider에 정의된 특정 속성 또는 기능을 가져와 사용하고 있다는 의미입니다.

 

public class InterfaceExample {
    public static void main(String[] args) {
        User user = new User(); // User 클래스 객체 생성
        user.callProvider(new Provider()); // Provider 객체 생성 후에 매개변수로 전달
    }
}

class User {
    public void callProvider(Provider provider){ //Provider 객체를 매개변수로 받는 callProvider 메서드
        provider.call();
    }
}

class Provider { // Provider 클래스
    public void call() {
        System.out.println("무야호~");
    }
}

 

위의 코드를 보면, User 클래스에 정의된 callProvider 메서드의 매개변수로 Provider 타입이 전달되어 호출되고 있는 것을 확인할 수 있습니다.

 

interface Cover { // 인터페이스 정의
    public abstract void call();
}
public class Interface4 {
    public static void main(String[] args) {
        User user = new User();
        user.callProvider(new Provider());
        user.callProvider(new Provider2());
    }
}

class User{
    public void callProvider(Cover cover){ // 매개변수의 다형성 활용
        cover.call();
    }
}
class Provider implements Cover{
    public void call(){
        System.out.println("무야호~");
    }
}

class Provider2 implements Cover{
    public void call(){
        System.out.println("야호~");
    }
}

 

먼저 Cover라는 인터페이스를 정의한 후에 각각의 구현체에 implements 키워드를 사용하여 각각 기능을 구현하고 있습니다.  User 클래스에서는 매개변수의 다형성을 활용하여 구체적인 구현체가 아닌 인터페이스를 매개변수로 받도록 정의했습니다.

이제 Provider 클래스의 내용 변경 또는 교체가 발생하더라도 User 클래스는 더 이상 코드를 변경해주지 않아도 같은 결과를 출력해 낼 수 있습니다.

 

 

인터페이스는 기능이 가지는 역할과 구현을 분리시켜 사용자로 복잡한 기능의 구현이나 교체/변경을 신경 쓰지 않고도 코드 변경의 번거로움을 최소화하고 손쉽게 해당 기능을 사용할 수 있도록 합니다. 선언과 구현을 분리시켜 개발시간을 단축할 수 있고, 독립적인 프로그래밍을 통해 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화할 수 있다는 큰 장점이 있습니다.

'공부 > JAVA' 카테고리의 다른 글

[Java] [레스토랑 키오스크 프로그램]  (0) 2024.07.02
[Java] 다형성  (0) 2024.07.02
[Java] 캡슐화,패키지,접근제어자,getter와 setter  (0) 2024.06.29
[Java] 상속(Inheritance)  (0) 2024.06.28
[Java] 내부 클래스  (0) 2024.06.26