■ 추상화
오늘은 객체지향 프로그래밍의 4가지 성질 중 하나에 해당하는 추상화에 대해서 서술하도록 하겠다. 여기서 우리는 "추상"의 사전적 의미를 먼저 생각해볼 필요가 있는데 추상의 사전적 의미는 "사물이나 표상을 어떠한 성질, 공통성, 본질에 착안하여 그것을 파악하는 것" 이라는 사전적 의미를 가지고 있다. 여기서 핵심적인 개념은 공통성과 본질을 추출하는 것 이다. 같은 맥락에서, 자바에서의 추상화는 객체의 공통적인 성질 또는 기능을 추출하여 정의하는 것을 의미한다.
OOP(Object Oriented Programming) 의 개념 중 하나인 상속이 하위클래스를 정의하기 위해 상위클래스를 사용하는 것 이라고 한다면, 반대로 추상화는 하위클래스들의 공통적인 요소들을 추출해 상위클래스를 만드는 것 이라고 할 수 있다. 사실 방법에 있어서 상향식 또는 하향식인지는 크게 상관없다. 즉, 공통적인 속성과 기능들을 활용하여 하위클래스를 만들 수 도 있고 또는 공통적인 속성과 기능들을 추출하여 상위클래스를 정의할 수도 있다.
위 그림은 추상화를 아주 잘 표현해주는 예시이다, 자동차와 바이크 공통적인 분모들을 모아 이동수단이라는 클래스에 담았다. 반대로 이동수단이 가지는 공통적인 속성 또는 기능을 자동차와 오토바이에 내려주었다 하여도 공식은 유효하다. 이렇게 공통적인 속성, 기능을 모아서 정의해주면 클래스간에 관계를 만드는데에도 유용하고, 데이터의 유지/보수에도 용이하며 클래스를 축약시키는데에도 아주 큰 도움이 된다.
■ abstract 제어자
추상화의 핵심개념을 본격적으로 공부하기에 앞서 우리가 먼저 알아야할 개념이 바로 abstract(추상적인) 제어자 이다. 자바의 제어자는 크게 접근제어자와 기타제어자 2가지로 구분할 수 있는데 기타제어자중 가장 빈번하게 사용되는 제어자가 바로 abstract 제어자 이다.
그럼 도대체 abstract 제어자가 무엇일까?
사전적의미에서 abstract 는 "추상적인" 이라는 뜻을 가지고 있는데, 자바의 맥락에서 abstract 단어가 내포하는 의미는 "미완성" 이라 정리 할 수 있다.
abstract는 주로 클래스와 메서드를 형용하는 키워드로 많이 사용되는데, 클래스 앞에 붙은 경우 추상클래스(abstract class), 메서드 앞에 붙은 경우 추상 메서드(abstract mathod)라고 부른다. 어떤 클래스에 추상메서드가 포함되어 있는 경우 해당 클래스는 자동으로 추상클래스로 변환이 된다.
아래 예시를 보도록하자
abstract class Abstruct{ //추상메서드가 최소 하나 이상 포함돼있는 추상클래스
abstract void Start(); //메서드바디가 없는 추상메서드
}
abstract에 가장 핵심적인 개념은 앞서 언급한 "미완성"에 있다. 추상 메서드는 메서드의 시그니쳐만 있고 메서드의 바디가 없는 메서드를 의미하는데, abstract 키워드를 앞에 붙여줌으로서 해당 메서드가 추상메서드 임을 명시한다. "구체적인"의 반대 의미로서 "추상적인" 이라는 형용사를 생각해보면 조금더 이해가 쉬울 것 이다. "추상적이다" 라는 말을 사전적 의미로 해석해 본다면 "충분히 구체적이지 않다" 라는 의미로 해석할 수 있는데 즉, 추상메서드는 "충분히 구체적이지 않은" 미완성 메서드라고 할 수 있으며 미완성 메서드를 포함하는 클래스는 "미완성 클래스(추상클래스->미완성설계도)" 라고 할 수 있다.
public class abstractTest {
Abstract abstract = new Abstract(); //에러발생
}
앞서 언급한 바와 같이 Absturct 클래스의 객체를 생성하려 하면 에러가 난다, 그 이유는 Absturct는 시그니쳐 바디만 존재하고 메서드바디가 존재하지않는 미완성메서드를 포함한 미완성된 설계도이기 때문이다. 즉, 추상클래스는 미완성클래스 -> 미완성설계도 라는 것이다.
그렇다면 추상화는 어떠한 이유 때문에 사용하는 것일까?
■ 추상클래스
앞서 위에 abstract 키워드를 서술하면서 나는 추상클래스란, 메서드 시그니쳐만 존재하고 메서드 바디가 존재하지않는 추상메서드를 포함하고 있는 미완성된 설계도 라는 것을 알 수 있었다. 또한 미완성된 메서드를 가지고 있기 때문에 이를 기반으로 객체를 생성할 수 없다는 것도 알게 되었다.
그렇다면 왜 객체를 생성하지도 못하는 추상화를 사용하는 것일까?
크게 두가지를 예로 들 수 있다. 추상클래스는 상속 관계에 있어 새로운 클래스를 작성하는데 아주 유용하게 쓰인다. 메서드의 내용이 상속받는 클래스에 따라서 종종 변하는 경우가 생기는데 상위클래스에서는 선언부만 작성하고, 실제 구체적인 내용은 상위클래스를 상속받는 하위클래스에서 구현하게 비워둔다면 설계하는 상황이 바꾸더라도 보다 유연하게 대처할 수 있다. 이때 사용할 수 있는 것이 메서드의 내용을 재정의 하는 키워드인 overriding 키워드이다. overriding을 통하여 추상클래스에서 상속받은 추상메서드의 바디 내용을 완성 시킬 수 있고
이렇게 생성이 된 클래스를 기반으로 해당 객체를 생성 할 수 있다.
package abstractstudy;
public class abstractAnimalTest {
public static void main(String[] args) throws Exception {
Animal dog = new Dog();
dog.sound();
Cat cat = new Cat();
cat.sound();
}
public 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 { // Animal 클래스로부터 상속
public Cat() {
this.kind = "포유류";
}
public void sound() { // 메서드 오버라이딩 -> 구현부 완성
System.out.println("야옹");
}
}
}
위의 예제에서 먼저 Animal 클래스 안에 abstract 키워드를 사용한 sound() 메서드가 추상 메서드로 선언되었고, 따라서 이를 포함하는 Animal 클래스 또한 abstract 키워드를 사용하여 추상 클래스로 만들어주었다. 그 이후 추상 클래스 Animal를 상속받은 Dog 클래스와 Cat 클래스 안에 추상 메서드 sound()를 각각 오버라이딩하여 각 객체에 맞는 구현부를 완성해주었고, 마지막으로 이렇게 완성된 클래스를 기반으로 dog 인스턴스와 cat 인스턴스를 생성하여 sound() 메서드를 호출했다. 그 결과 출력값으로 각각 “멍멍"과 “야옹"이라는 값이 반환되었다. 이렇듯 추상 클래스를 사용하면 상속을 받는 하위 클래스에서 오버라이딩을 통해 각각 상황에 맞는 메서드 구현이 가능하다는 장점이 있다.
두 번째로, 추상 클래스는 자바 객체지향 프로그래밍의 마지막 기둥인 추상화를 구현하는데 핵심적인 역할을 수행한다. 추상화를 한마디로 정리하면 “객체의 공통적인 속성과 기능을 추출하여 정의하는 것”이라 정리할 수 있다. 앞선 예시를 다시보면, 동물이 가지는 공통적인 특성을 모아 먼저 추상 클래스로 선언해주었고, 이를 기반으로 각각의 상속된 하위 클래스에서 오버라이딩을 통해 클래스의 구체적인 내용을 결정해주었다. 만약 여러 사람이 함께 개발하는 경우, 공통된 속성과 기능임에도 불구하고 각각 다른 변수와 메서드로 정의되는 경우 발생할 수 있는 오류를 미연에 방지할 수 있다.
결론적으로 구체화에 반대되는 개념으로 추상화를 생각해보면, 상속계층도의 상층부에 위치할 수록 추상화의 정도가 높고 그 아래로 내려갈수록 구체화된다고 정리해볼 수 있다. 다른 말로, 상층부에 가까울수록 더 공통적인 속성과 기능들이 정의되어 있다고 생각할 수 있다.
//예시 2
package abstractstudy;
public class abstractCarTest {
public static void main(String[] args) {
Car sportsCar = new SportsCar();
sportsCar.sound();
Car jeep = new OffroadCar();
jeep.sound();
}
}
abstract class Car{ // 메서드 시그니처만 있는 추상클래스
String kind;
abstract void sound(); //바디가 없는 추상메서드
}
class SportsCar extends Car{ //Car 클래스로부터 상속
SportsCar(){ // 생성자 작성
this.kind = "스포츠카";
}
void sound(){
System.out.println("스포츠카 부릉부릉"); //메서드 오버라이딩 ->구현부 작성
}
//public String toString(){return "포르쉐";}
}
class OffroadCar extends Car{
OffroadCar(){
this.kind = "오프로드";
}
void sound(){
System.out.println("오프로드 부앙부앙");
}
//public String toString(){return "지프";}
}
//결과값
스포츠카 부릉부릉
오프로드 부앙부앙
■ final 키워드
자바 객체지향 프로그래밍에서 가장 중요하다는 인터페이스 클래스를 서술하기전 인터페이스를 이해하기 위해서 반드시 짚고 넘어가야하는 final 키워드에 대해서 먼저 서술하겠다.
영어로 "최종의","마지막" 이라는 뜻을 가지고 있는 final 키워드는, 필드,지역변수,클래스 앞에 위치할 수 있으며 그 위치에 따라 코드의 내용이 조금씩 달라지게 된다.
위치 | 의미 |
클래스 | 변경 또는 확장 불가능한 클래스, 상속 불가 |
메서드 | 오버라이딩 불가 |
변수 | 값 변경이 불가능한 상수 |
위 표를 보면 각각의 차이는 있지만, 확장 불가능하고 확장 할 수 없다는 점에서 유사한 성질을 띄고 있다는 것을 확인 할 수 있다.
final class FinalEx{ //확장 및 상속이 불가능한 클래스
final int i = 3; //값 변경이 불가능한 변수
final void getNum(){ //오버라이딩이 불가능한 메서드
final int localInt = i; //상수
return i;
}
}
위 표를 코드로 작성하였다. 위에 코드와 표를 보면 알 수 있듯이 final 키워드의 위치에 따라서 그 결과값이 달라지는 것을 확인할 수 있다.
■ InterFace
Interface는 객체지향 프로그래밍 추상화에서 매우 중요한 역할을 하는 키워드이다. 인터페이스는 사전적 의미로 "서로 다른 장치,소프트웨어 등을 서로 따위를 서로 이어주는 그런 접속 장치"를 의미하며 자바에서의 인터페이스도 그 의미와 크게 다르지 않게 서로 다른 두 매체간에 연결을 시켜주는 중간다리,가교 역할을 수행해 주고있다.
흔히 인터페이스를 학습하기에 앞서 종종 인터페이스와 추상화를 비교하기도 한다. 그이유는 인터페이스도 자바와 추상화와 비슷하게 자바의 추상화를 구현한다는 점에서 공통점이 있기 때문이다. 하지만 인터페이스는 추상화와 달리 더 높은 추상점을 가진 추상화를 구상한다는데 있어서 추상화와는 아주 큰 차이점이 있다.
추상클래스의 구현부가 미구현된 메서드와 멤버 변수를 가질 수 있는 반면, 인터페이스는 오직 상수와 추상메서드만 가질 수 있고 그 이외 요소는 가질 수 없다는 특징이 있다.
이제 본격적으로 인터페이스의 구조를 알아보도록 하자, 인터페이스를 선언하는 것은 클래스와 유사하지만 선언 시 클래스는< class 클래스명 > 으로 작성되는 것과 다르게 인터페이스는 < interface 인터페이스명 > 으로 작성 한다는 점에서 차이점이 있으며 또 일반 클래스와 다르게 내부 모든 필드가 public static final 로 정의되고 static , dafult 외에 모든 메서드가 public abstruct 로 정의된다는 차이가 존재한다. 다만 인터페이스의 모든 메서드와 필드는 위 값들이 내포되어있기 때문에 생략할 수 있다.
package interfacee;
public abstract class interfaceTest {
public static final int rock = 1; //인터페이스 인스턴 변수 정의
final int scissors = 2; // public static 생략
static int paper = 3;
public abstract String getPlayNum();
abstract void call(); //public 생략
}
위 코드를 보면 상수와 구현되지 않은 추상메서드만 포함되어있는 것을 확인 할 수 있다. 인터페이스에서 상수를 구현할 때는 반드시 앞에 public static fianl 를, 메서드를 구현할때는 반드시 앞에 abstract 로 정의해주어야 하지만, 위 코드를 보는 바와 같이 일부분 또는 전부를 생략할 수 있으며 비어진 부분은 컴파일러가 자동추가를 해준다.
■ InterFace 구현
위에서 설명한 방법으로 인터페이스를 정의하였다면, 이제는 인터페이스의 구현방법을 알아보자. 어떤 클래스가 어떤 인터페이스를 구현한다는 것은 인터페이스를 상속 받은 클래스에서 인터페이스 내에 추상메서드들을 모두 오버라이딩 해야하다는 것 이다.
class test implemnts InterfaceTest { ... }
클래스 간에 상속은 단 하나만 가능하다. 즉 하위클래스는 상위클래스 단 하나만을 상속받을 수 있다는 말이다. 그렇지만 인터페이스는 한 클래스에서 다중적 구현이가능하다. 즉 하나의 클래스에서 여러가지의 인터페이스를 구현할 수 있다는 것이다.
class MultyTest implemnts InterfaceTest1, InterfaceTest2 { ... }
아래 코드를 살펴보자.
package interfacee;
public class interfaceCarTest {
public static void main(String[] args) {
Mustang mustang = new Mustang();
mustang.model();
mustang.Sound();
System.out.println(); //줄바꿈
Jeep jeep = new Jeep();
jeep.model();
jeep.Sound();
}
}
interface Car{public abstract void model();} // public abstract 생략가능
interface ExhaustSound{void Sound();}
class Mustang implements Car,ExhaustSound { //Car와 ExhaustSound 다중구현
@Override
public void model() { //메서드 오버라이드
System.out.println("머스탱");
}
public void Sound(){
System.out.println("부아아아부아아방방방");
}
}
class Jeep implements Car,ExhaustSound{
@Override
public void model() {
System.out.println("지프");
}
public void Sound(){
System.out.println("그릉그릉 그르릉");
}
}
//출력값
머스탱
부아아아부아아방방방
지프
그릉그릉 그르릉
위 코드를 살펴보면 클래스로 Mustang 과 Jeep 클래스가 있다. 각각의 클래스는 인터페이스 Car,ExhaustSound를 다중으로 구현하여 가각각 객체에 맞는 메서드를 오버라이딩 하고, 그 값을 출력값으로 돌려주고 있다.
클래스는 상속을 한가지만 받을 수 있는데, 어째서 인터페이스는 다중 구현이 가능할까?
클래스가 다중 상속이 불가능한 이유는 만약 부모 클래스에 동일한 이름의 필드 또는 메서드가 존재하는 경우 충돌이 발생하기 때문이다, 하지만 인터페이스는 추상메서드로 애초에 미완성된 멤버를 가지고 있기 때문에 충돌이 발생할 여지가 없고 따라서 클래스에서 다중구현이 가능한 것이다. 나아가, 클래스를 상속받으며 동시에 인터페이스를 구현 할 수 도 있다.
package interfacee;
public class interfaceCarTest {
public static void main(String[] args) {
Mustang mustang = new Mustang();
mustang.model();
mustang.Sound();
System.out.println(); //줄바꿈
Jeep jeep = new Jeep();
jeep.model();
jeep.Sound();
}
}
abstract class Car{public abstract void model();} //추상메서드
interface ExhaustSound{public abstract void Sound();} // public abstract 생략가능
class Mustang extends Car implements ExhaustSound { //Car 클래스상속 ExhaustSound 구현
@Override
public void model() { //메서드 오버라이드
System.out.println("머스탱");
}
public void Sound(){
System.out.println("부아아아부아아방방방");
}
}
class Jeep extends Car implements ExhaustSound{
@Override
public void model() {
System.out.println("지프");
}
public void Sound(){
System.out.println("그릉그릉 그르릉");
}
}
위 코드를 보면, 위에 클래스에서 인터페이스였던 Car 인터페이스를 추상클래스로 전환하였다. 그러고 나서 Mustang 과 Jeep 클래스가 Car 추상클래스를 상속받게 만들었고 여전히 인터페이스 ExhaustSound 를 구현하게 하여, 같은 결과물을 출력하였다.
'Programming > Cs' 카테고리의 다른 글
정수 자료형, 문자열, 논리 자료형 (1) | 2023.10.27 |
---|---|
[Git] 깃허브 명령어 정리 (0) | 2022.09.17 |
[OOP] 다형성 (2) | 2022.05.13 |
[지식] this vs this() (1) | 2022.05.11 |
[지식] 클래스 (Class) 와 객체 (Object) (2) | 2022.05.10 |