자바 기초 정리 : 상속, 인터페이스

    객체 지향 프로그래밍


    객체 지향 프로그래밍은 각 항목들이 각각의 성질을 가지는 객체로 보고, 그 객체들 간의 상호작용을 구현하는 프로그래밍이다. 즉, 객체간의 협력이 일어나는 것을 묘사하는 것으로 보면 된다. 실제로 객체 지향 프로그래밍은 우리 삶과 매우 닮아있다. 예를 들어 회원 로그인을 하고 여러 판매자가 팔고 있는 상품 중 하나를 골라 주문하는 경우가 있다. 이를 객체 지향 프로그래밍으로 뜯어볼 수 있다.

    "회원 객체"가 홈페이지에서 "판매자 객체"가 판매하고 있는 "상품 객체"를 골랐다고 볼 수 있다. 객체 지향 프로그래밍은 이런 객체 관점에서 뜯어본다면 회원 객체와 판매자 객체, 그리고 상품 객체가 서로 상호작용을 하는 것을 구현하는 프로그래밍이라고 볼 수 있다.

    이런 객체 지향의 핵심은 1) 상속 2)정보은닉 3)다형성을 구현하는 것에 있다고 한다. 1) 상속은 객체 지향이 가지고 있는 특성이며, 2) 정보은닉은 클래스 내부에서 Private, Protected 지시어를 활용해 정보 공개 범위를 설정할 수 있다. 3) 다형성은 상속, Override,  UpCasting을 이용해서 구현할 수 있다. 

     

    클래스 상속 


    • 클래스 상속(Inheritance)는 상위 클래스가 가지고 있는 성질을 하위 클래스에서 사용할 수 있도록 해주는 것이다.
    • 좀 더 일반적인 클래스에서 구체화된 클래스를 구현할 때, 일반적인 클래스를 상속시켜서 쓴다. 즉, 확장의 개념이 된다.
    • 상속의 문법은 'Class A extends B'으로 사용가능하다.
    • 자바에서는 단일 상속만 가능하다. 이는 Diamond Problem 때문이다. Diamond Problem이라는 것은 조상 클래스, 2개의 부모 클래스, 그리고 자식 클래스가 있다고 가정하고 조상 클래스와 부모 클래스들이 동일한 메서드가 구현이 되어있다고 하면, 자식 노드에서 그 메서드를 호출했을 때 어떤 메서드를 사용하는지 모호하다는 문제이다. C++에서는 가능하지만, 자바에서는 이런 이유로 단일 상속만 가능하다.
    • 상속을 사용하게 되면 다형성이 확보되기 때문에 코드의 유지보수, 확장 관점에서 이점이 있다.
    • 상속을 할 때, 정보은닉이 필요하다면 변수에 protected를 사용한다. protected는 외부에서는 접근이 불가능하나, 클래스 내부와 하위 클래스가 상위 클래스로 접근은 가능하다.
    • 상속 관련 다이어그램은 아래 그림처럼 표기한다. 상속받는 쪽이 상속하는 쪽을 가리킨다.
    • 상속 클래스는 여러 층으로 나뉘어질 수는 있다. 단, 상속의 깊이가 깊어지는 쪽은 좋지 않은 프로그래밍 방향이다.

    상속 관련 예제


    • Customer 클래스가 상위 클래스, VIP 고객 클래스가 있다. 일반 고객은 1% 적립, VIP 고객은 10%가 적립되며 전문 상담원이 배정되며, 물건 구매시 10%가 할인된다. 이런 경우, 일반 고객 클래스에서 상속받아 VIP 클래스를 구현하면 된다.
    • 클래스 다이어그램은 아래와 같이 작성할 수 있다. 아래 다이어그램을 바탕으로 클래스 사이의 코드를 짜면 다음과 같다.

    // 일반 고객 클래스 구현
    
    public class Customer {
    	
    	public int customerId;
    	public int customerPoint;
    	public int pointRatio;
    	public String Grade;
    	
    	public Customer(int customerId) {
    		Grade = "NORMAL";
    		this.customerId = customerId;
    	}
    	public int price(int price) {
    		customerPoint = (int)(price * pointRatio);
    		return price;
    	}
    }
    
    // VIP 고객 클래스 구현 (일반 고객 클래스 상속)
    public class VIPCustomer extends Customer{
    
    	public String agent ; 
    	public double salesRatio ; 
    	
    	public VIPCustomer(int customerId) {
    		super(customerId);
    		grade = "VIP";
    		agent = "agentLee";
    		pointRatio = 0.1;
    		salesRatio = 0.1;
    	}
    	public int price(int price) {
    		customerPoint += (int)(price * pointRatio);
    		return (int)(price - salesRatio*price);
    	}
    }
    
    // GOLD 고객 클래스 구현 (일반 고객 클래스 상속)
    public class GoldCustomer extends Customer {
    	public GoldCustomer(int customerId) {
    		super(customerId);
    		grade = "GOLD";
    		pointRatio = 0.05;
    	}
    }
    
    //TEST 함수
    
    public class CustomerTest {
    	public static void main(String[] args) {
    		VIPCustomer customerLEE = new VIPCustomer(12345);
    		GoldCustomer customerKim = new GoldCustomer(123457);
    		Customer customerCHO = new Customer(123456);
    		
    		System.out.println(customerLEE.price(10000));
    		System.out.println(customerLEE.customerPoint);
    		System.out.println(customerKim.price(10000));
    		System.out.println(customerKim.customerPoint);
    		System.out.println(customerCHO.price(10000));
    		System.out.println(customerCHO.customerPoint);
    		
    		
    	}
    }

     

    상속 클래스의 생성 과정


    • 클래스는 생성자를 통해 생성한다. 상속 클래스 역시 생성자를 통해 생성한다. 이 때, 생성자가 실행되는 순서가 있다. 예를 들어 하위 클래스의 생성자가 실행된다면 1) 상위 클래스의 생성자가 실행된 후 2) 하위 클래스의 생성자가 실행된다.
    • 하위 클래스의 생성자는 일반적으로 super()라는 Default 생성자가 들어가야한다. default 생성자라면 super()를 넣지 않아도 컴파일러가 생성해주지만, 매개변수를 받는 생성자라면 하위 클래스의 생성자를 재정의 해주면서 super()를 사용해주어야 한다.
    • super()는 하위 클래스에서 사용되며 1) 상위 클래스의 참조값을 가지기도 하고 2) 생성자 기능을 가지기도 한다. 1)의 용도로 사용할 때는 "super. "으로 사용하며, 2)의 기능으로 사용할 때는 super()로 사용한다.
    • 아래 예시에서는 super()로 상위 클래스의 생성자를 생성하는 코드다. 생성자 실행이 어떻게 진행되는지 알 수 있다.
    // 아래코드에서 VIPCustomer 인스턴스를 만들면 output은 다음과 같이 나온다
    output >>>
    "Customer 생성자 실행"
    "VIP Customer 생성자 실행"
    
    
    //=====================================
    // 상위 클래스 default 생성자가 없고, 매개변수 생성자이기 때문에 
    // 하위 클래스에서 super(매개변수)로 상위 클래스 인스턴스를 생성해준다.
    public class VIPCustomer extends Customer{
    
    	public String agent ; 
    	public double salesRatio ; 
    	
    	public VIPCustomer(int customerId) {
    		super(customerId);
    		grade = "VIP";
    		agent = "agentLee";
    		pointRatio = 0.1;
    		salesRatio = 0.1;
    		super.customerId
            System.out.println("VIP Customer 생성자 실행")
    	}
    }
    /// 상위 클래스
    public class Customer {
    	
    	protected int customerId;
    	public int customerPoint;
    	protected double pointRatio;
    	protected String grade;
    	
    	public Customer(int customerId) {
    		grade = "NORMAL";
    		this.customerId = customerId;
    		pointRatio = 0.01;
            System.out.println("Customer 생성자 실행")
    	}
    }

     

    상속 클래스를 만들었을 때, 인스턴스 메모리 상태


    • 인스턴스는 모두 Heap Memory에 선언됨.
    • 생성된 인스턴스는 상위 클래스 생성자인 super()로 멤버 변수가 생성되고, 나머지 하위 클래스 멤버변수가 생성됨.

     

    클래스의 형변환, 업 캐스팅


    • 상속 클래스는 형변환이 가능하다. 상위 클래스에서 하위 클래스로 바뀌는 것은 다운 캐스팅, 하위 클래스에서 상위 클래스로 바꾸는 것은 업 캐스팅이라고 한다.
    • 업 캐스팅은 묵시적 변경이 가능, 다운 캐스팅은 묵시적 변경이 불가능하다. 따라서 다운 캐스팅 할 때는 명시적으로 변형하고자 하는 타입으로 변경해줘야 한다. (Student str1 = (Student) str2, 여기서 str2는 Object 클래스)
    • 하위 클래스가 업캐스팅 되면 하위 클래스는 하위 클래스에만 있는 멤버 변수, 멤버 메서드는 사용할 수 없다. 
    • 업 캐스팅을 사용하는 이유는 다형성을 구현하기 위함이다.
    // 상위 클래스 : Customer
    // 하위 클래스 : VIPCustomer
    
    // customer의 변수 타입은 Customer
    // customer는 VIPCustomer 타입이지만, Customer Class의 변수, 메서드만 사용 가능함.
    Customer customer = new VIPCustomer();

     

    Override, Method의 재정의


    • 상위 클래스에게 메서드를 상속받았다고 그대로 사용할 필요는 없다. 메서드를 재정의해서 사용하면 되고, 이를 Override라고 한다. 
    • 메서드 타입, 메서드 이름, 매개변수를 동일하게 해주고 메서드 앞에 @Override를 작성해주어 컴파일러에게 재정의된 메서드임을 알려줘야 한다.
    • Overriding 되었을 때 메서드를 호출하면, 인스턴스를 기준으로 메서드가 실행된다(가상 메서드의 원리). 형변환과 Overriding의 관계는 아래 참조.(단, Child Class에 Method 재정의가 이루어짐.)
      1) Parent Class 선언 + Parent Class 생성 → Parent Method 사용
      2) Child Class 선언 + Child Class 생성 → Child Method 사용
      3) Parent Class 선언 + Child Class 생성 → Child Method 사용

     

    가상 메서드의 원리

    • Java는 가상 메서드 테이블에 메서드 이름과 메서드 주소값을 가짐.
    • 재정의된 메서드는 재정의된 주소를 가리킴.
    • 인스턴스는 변수와 메서드로 나누어진다. 변수는 Heap 메모리에 저장되며, 메서드는 Code 영역에 저장된다.
    • 메서드를 호출하면, 메서드의 이름과 맵핑된 주소가 호출된다. 메소드 Instruction Set은 Code 영역에 있고, 그 주소를 찾가서 호출됨.
    • 각 인스턴스가 가지는 데이터는 달라도 메서드는 동일하다. 따라서, 메서드는 메서드 영역에 한번만 Load되며, 인스턴스가 메서드를 호출하면, Code 영역에 있는 메서드의 코드를 가리키는 주소가 호출된다.

    좌 : 가상 메서드 / 우 : 각 변수 및 Method의 위치

     

    다형성(Polymorphism)

    • 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 의미한다. 다형성을 구현하게 된다면, 같은 코드에서 여러 실행 결과를 볼 수 있게 된다.
    • 다형성은 If / Else 구문으로 구현할 수도 있으나, 이는 확장성과 유지보수 관점에서 좋지 않다. 따라서 클래스의 상속과 Method 재정의, 그리고 업캐스팅을 활용해 다형성을 구현할 수 있다.
    • 상위 클래스에서는 하위 클래스에서 공통적으로 사용될 것을 정의하고, 하위 클래스에서는 각 클래스에 맞는 기능을 구현한다. 그리고 업캐스팅을 활용해 하나의 타입으로 핸들링해 구현 가능하다. 

    동물이라는 상위 클래스가 있고, 하위 클래스로 사자, 고래, 사람, 독수리가 있다고 가정하자. 이 때, 상위 클래스에서 'move()'라는 메서드가 정의되었다. 이 때, 사자는 "네 발로 걷습니다"를, 고래는 "지느러미 헤엄 칩니다", 사람은 "두 발로 걷습니다"를, 독수리는 "두 날개로 하늘을 납니다."라는 결과가 나오게 하고 싶다.

    이 때, 상위 클래스의 move() Method에서 If Else 문을 사용해 사자, 고래, 사람, 독수리인지 판단하고 그 값에 따라 결과물을 출력할 수도 있다. 그렇지만 이는 확장성 및 유지보수 관점에서 지양해야하는 방법이다.

    그 대신에 move()라는 메서드를 선언한 후, 하위 클래스에서 move() Method를 재정의해준다. 재정의한 후, 동물형 자료에 사자, 고래, 사람, 독수리를 각각 대입하고(Up Casting) move 메서드를 실행하게 되면 원하는 결과가 나온다. 이 방법의 장점은 여러 기능 추가와 새로운 요소를 추가하기 쉽다는 점이다.

    예를 들어 거북이에 대한 판별도 필요하다고 가정하자. 이 때, 하위 클래스에 거북이를 만들고 동물을 상속 받게 한 후에 move() 메서드를 재정의 해주면 된다. 

     

    상속 사용 시점 

    • Is - A 관계 : 상속(Inheritance)
      일반적인 관계, 구체적인 관계를 클래스가 유지할 때.
    • Has - A 관계 : 조합(Composition)
      클래스가 다른 클래스의 인스턴스를 포함하는 관계. 상속받지 않고, 인스턴스를 선언한 후 메서드를 가져다 쓰는 방식.예를 들어, 성적 관리 프로그램에서 Student 클래스에 Subject(과목) 클래스를 가져다 쓰는 경우

     

    Down Casting과 Instanceof 

    • Upcasting은 하위 클래스를 상위 클래스로 바꾸는 것. Down Casting은 상위 클래스를 하위  클래스로 바꾸는 것이다.
    • Down Casting은 UpCasting된 인스턴스를 다시 원래 타입으로 형변환할 때 사용한다. 예시는 최상위 클래스인 Object 클래스를 원래 클래스로 원복.
    • 형변환 과정에서 instanceof 명령어를 사용한다. instanceof 명령어는 같은 형인지를 확인하고 boolean 값을 return해주는 메서드이다. instanceof로 같은 형인 것이 확인되면 Down Casting을 해주어야 에러가 발생하지 않는다.
    • 사용 예시는 animal(인스턴스) instanceof Animal(타입)이다.

     

    Abstract Class(추상 클래스)

    • Abstract Class는 클래스 내부에 Abstract Method를 가진 클래스다.
    • Abstact Method는 선언부만 있고, 구현부는 선언되지 않은 메서드다. ( publid a(); )
    • Abstract Class는 Abstract Method가 있기 때문에 인스턴스 선언이 되지 않는다. 상속을 위해 만들어진 클래스이며, 인터페이스와 유사한 기능을 하는 것으로 이해할 수 있다.

    Abstract Class / Concrete Class 도식화

    Abstract Class의 응용 : Templete Method Pattern

    • 라이브러리와 프레임워크 개념이 있음. 라이브러리는 JDK에서 제공되는 것이며, 프로그램의 Flow를 프로그래머가 결정할 수 있음. 반면, 프레임워크는 스프링부트 같은 것들이 있는데 프로그램의 Flow는 이미 정해져있고, 잔가지들만 프로그래머들이 구현함. Templete Method Pattern은 프레임워크처럼 이미 틀을 정해주고, 그에 대한 구현을 프로그래머들이 한다.
    • Templete Method는 Abstract Class에 틀을 구현해주고, 이 메서드를 상속받은 클래스에서 구현해서 사용하는 형태로 사용한다.
    • Templete Method에서 함수 실행 순서는 변하면 안되기 때문에 Final로 사용을 해준다.
    • 필요하다면 멤버 변수를 Static으로 선언하는 것도 고려할 수 있다. 추후 인스턴스 선언 없이도 Class이름.변수명으로 참조값을 불러올 수 있기 때문이다.

     

    인터페이스(InterFace) 

    • 인터페이스는 주로 설계에 사용된다.
    • 인터페이스는 Abstract Method만 가지고 있으며, 모든 상수는 Public이다. 명시하지 않을 경우 컴파일러에서 자동으로 Method는 public Abstract, 상수는 Public static final로 설정해준다.
    • 인터페이스는 Abstract Method만 있기 때문에 멤버 변수는 없다. 
    • 인터페이스는 구현 코드가 없다. 따라서 Diamond Problem이 발생하지 않기 때문에, 한 클래스가 여러 개의 Interface를 구현해서 사용할 수 있음.

     

     

    인터페이스의 정의와 구현

    인터페이스를 구현한 클래스는 인터페이스 형으로 선언한 변수에 대입이 가능하다. 이것은 상속에서의 Upcasting과 동일한 것이다. 

    인터페이스는 틀만 제공해주고, 아래 클래스들이 인터페이스를 구현해 사용한다. 이 때, 하위 클래스에서 인터페이스에 정의된 메서드 외의 메서드를 정의할 수 있다. 이 때, 하위 클래스 인스턴스가 하위 클래스에 담길 경우에는 메서드를 모두 사용 가능하다. 단, 인터페이스 타입에 하위 클래스 인스턴스가 초기화 된다면, 하위 클래스에만 구현된 메서드는 사용할 수 없다.

    인터페이스와 추상 클래스의 차이는? 

    인터페이스와 추상 클래스의 가장 큰 차이는 인터페이스는 다중 구현이 가능하지만, 추상 클래스는 다중 구현이 불가능하다는 점이다. 예를 들어, 사람이란 가상 클래스를 구현한다고 가정하면, 어떤 사람은 영어를 구사할 수 있고, 어떤 사람들은 프로그래밍을 할 수도 있다. 이 상황을 가상 클래스들로 구현할 경우 영어를 할 줄 아는 사람 클래스, 프로그래밍을 할 줄 아는 사람 클래스를 구현하면, 영어와 프로그래밍을 동시에 할 수 있는 사람을 구현하려면 새로운 가상 클래스를 만들어야 한다.

    이 때는 사람이라는 추상 클래스를 구현하고, 그 추상 클래스가 영어 인터페이스, 프로그래밍 인터페이스를 구현하면 된다. 그리고 사람 추상 클래스를 상속받은 새로운 클래스를 만들 때, Upcasting을 해서 영어 타입으로 변경해서 영어를 사용할 수 있는 사람으로 바꾸면 된다. 

    인터페이스를 이용한 다형성 구현

    인터페이스를 이용한 다형성 구현은 하나의 인터페이스를 다양한 클래스가 구현했을 경우, 사용자는 그 클래스를 Upcasting으로 인터페이스 타입으로 선언한 후 동일한 코드를 실행하기만 하면 다양한 결과를 받을 수 있다.

    인터페이스를 활용한 다형성 구현 : Dao 구현하기

    추후 업데이트

    인터페이스의 여러가지 요소

    • 1. 상수 : 인터페이스의 모든 변수는 상수로 선언됨 (public Static이 Default)
    • 2. 메서드 : 인터페이스의 모든 메서드는 Abstract Method로 선언됨 (public abstract가 Default)
    • 3. Default Method : 구현하는 클래스들이 공통적으로 사용할 Method는 Interface에서 구현한다.
      default 키워드로 인터페이스에서 선언하며, 구현 클래스에서 Override 가능함.
    • 4. Static Method : Default Method는 인스턴스가 만들어져야 사용이 가능하다. Static Method는 프로세스 시작 시, Static 영역에 할당되기 때문에 인스턴스 없이 클래스 이름으로 접근 가능함.
    • 5. Private Method : Private Method는 interface 구현 클래스에서 재정의, 사용 불가능(Interface 내부에서만 사용하기 위해 구현). 주로 Interface의 default, static Method에서만 사용됨.

    여러 인터페이스의 구현

    • 인터페이스는 구현코드가 없기 때문에 다중 구현, 다중 상속이 가능하다.
    • 인터페이스가 다중 구현될 때, Default Method가 중복되는 경우가 있다. Default Method가 중복되는 경우, 구현 클래스에서 Default Method를 재정의해서 사용한다.
    • 여러 인터페이스를 구현한 클래스가 있고, 이 클래스의 인스턴스가 하나의 인터페이스 타입으로 업캐스팅 되면 그 인터페이스를 구현한 메서드만 사용이 가능하다.
    • 인터페이스의 상속은 extends로 하며, 구현 코드가 없기 때문에 다중 상속이 가능하다. 이런 이유로 Type 상속이라고 한다. 상속된 인터페이스를 구현한 클래스는 상속 받은 클래스의 모든 Method가 사용가능하다.

    Interface의 Default Method 관련 코드 예시

    public interface English {
    	public default void doing() {
    		System.out.println("영어를 합니다.");
    	}
    }
    
    public interface Program {
    	public default void doing() {
    		System.out.println("프로그래밍을 합니다.");
    	}
    	
    }
    
    // 구현 시, 오류 메세지 뜨며 Override 권유됨.
    // 동일한 이름의 Default Method가 구현한 두 개의 Interface에 모두 있기 때문
    // 이 경우는 구현 클래스에서 Default Method를 재정의해서 사용한다.
    public class Human implements English, Program {
    	@Override
    	public void doing() {
    		English.super.doing();
    	}
    	
    }

     

     

    클래스 상속, 인터페이스 구현 동시에 사용하기

    한 클래스를 상속받은 하위 클래스가 다른 인터페이스를 구현할 수 있다.

    아래 Class Diagram을 구현해본다.

    // 상위 클래스 구현
    public class Shelf {
    	
        // 멤버변수는 ArrayList
        // 무슨 책이 들어있는지 보여주는 showBookInfo() 구현
        // 보관중인 책의 권수를 알려주는 showBookCount() 구현
    
    	ArrayList<String> shelf = new ArrayList<>(); 
    	public int showBookCount() {
    		return shelf.size();
    	}
    	
    	public String showBookInfo(int index) {
    		return shelf.get(index);
    	}
    }
    
    public interface Que {
    	// enque, deque만 정의
    	public void enque(String name);
    	public String deque();
    }
    
    //하위 클래스 구현 (Shelf 상속 + Que 구현)
    public class BookShelf extends Shelf implements Que {
    	// interface 영역은 Override로 구현
    	@Override
    	public void enque(String name) {
    		super.shelf.add(name);
    	}
    	@Override
    	public String deque() {
    		return super.shelf.remove(0);
    	}
    	
    }
    
    
    //Test Code
    //1. 책 다섯 권을 넣음
    //2. 들어있는 책이 무엇인지 확인
    //3. 들어있는 책 권수 확인
    //4. 들어있는 책을 1권 뺌.
    public class BookShelfTest {
    	public static void main(String[] args) {
    		BookShelf bookshelf = new BookShelf();
    		
    		bookshelf.enque("반지의 제왕1");
    		bookshelf.enque("반지의 제왕2");
    		bookshelf.enque("반지의 제왕3");
    		bookshelf.enque("반지의 제왕4");
    		bookshelf.enque("반지의 제왕5");
    		
    		for (int i = 0 ; i < bookshelf.showBookCount() ; i ++) {
    			System.out.println(bookshelf.showBookInfo(i));
    		}
    		System.out.println(bookshelf.showBookCount());
    		System.out.println(bookshelf.deque());
    		System.out.println(bookshelf.showBookCount());
    	}
    }

    위의 내용을 코드로 구현했다. 자바에 능숙한 사람들은 괜찮겠지만, 아직 익숙하지 않은 사람들은 멤버 변수를 다룰 때 조심해야한다. 처음 구현할 때, ArrayList Type의 변수를 선언만 해두었는데, 초기화를 하지 않아 null로 인해 Error가 발생했다.

     

     

     

     

     

    '프로그래밍 언어 > JAVA' 카테고리의 다른 글

    자바의 Object Class  (0) 2021.10.23
    Java의 여러 내부 클래스 정의, 유형  (0) 2021.10.23
    Java의 Lambda Expression(람다식)  (0) 2021.10.22
    Java의 Reduce 정의  (0) 2021.10.22
    자바 강의 듣고 정리  (0) 2021.10.19

    댓글

    Designed by JB FACTORY