[Java Tips] 다시 정리하는 Java 객체지향

Sierra·2023년 1월 20일
0

JAVA Tips

목록 보기
2/3
post-thumbnail

Intro

학부 시절에는 클래스에서 객체를 만들어다 쓰는 것 이상을 해 본적이 거의 없었다.
막상 돌아보면 당시에 그것을 넘어 설 만한 작업을 할 일은 없었으니까.

한계를 깨달은 건 3학년 때 앱 클라이언트를 개발 할 때였다. 기존에 알고있던 지식 만으로는 코드가 우아하게 나오지 않았다. 뭔가 작동은 하는데 로직은 이상했다. 당시에는 무슨 이유 때문일 지 상당히 고민스러웠다.

학부 때 많이 하는 실수 중 하나다. 알고있는 지식이 전부라고 생각하는 것. 당시에 객체지향에 대해 그 정도밖에 몰랐기 때문에 로직도 코드도 이상한 코드가 나타난 것이다. 당시에 팩토리 패턴을 이해했다면, 코드가 좀 더 우아했을 것이고 로직이 꼬이지도 않았을 것이다.

객체지향이라는 말의 의미를 온전히 깨달을 때 까지는 오랜 시간이 걸린다. 단순히 클래스에서 객체를 꺼내다 쓴다 라는 개념만을 가지고 객체지향이란 걸 이해하기엔 그렇게 객체지향이 단순한 개념은 아니라고 생각한다.

객체의 의미를 제대로 이해해야지 객체지향을 쓰는 의미가 있다는 것을 실무에서 정말 많이 깨닫고 와닿았다. 필자는 한때 AI 분야에 관심이 있었다.

소프트웨어 개발자가 아닌 데이터 분석가의 길을 가려고 한때 소프트웨어개발을 상당히 오래 손을 놓은 적이 있었다. 데이터 분석에 필요한 코딩과 소프트웨어 개발에 필요한 코딩의 차이는 상당히 컸었다. 1년 가까이 손을 때고 취업 준비를 하던 시점에 다시 백엔드 개발자의 진로를 걷다보니 학부 시절과는 다르게 객체지향이란 개념의 방대함에 대해 상당히 많이 깨달았었다.

이번 포스팅은 여러 파트로 나뉜다. 먼저 기본적인 객체지향에 대한 개념 정리, JAVA에서의 클래스에 대한 기초적인 개념 정리 후 클래스를 생성하는 여러가지 방법에 대해 정리하려 한다

자바는 객체지향 언어인가?

너무 당연한 소리지만 100%는 아니다. 원시 타입은 객체로 취급하지 않는다.

int a = 10;

a는 객체인가? 아니다. 원시타입 정수 변수일 뿐이다.
그럼 객체로 인정되는 기준은 무엇일까? 그 전에 객체가 뭔지 한번 다시 짚고 넘어 가보자.

우선 사전적 정의로써 객체는 클래스의 인스턴스나 배열을 말한다 라고 한다.

"교수님, 클래스가 뭔가요?"
"반!"
"그럼 객체가 뭔가요?"
"학생!"

실제로 아는 사람이 모 교수님께 질문했던 일화에서 가져왔다. 객체에 대해 이해하고 있다는 가정 하에 저런 비유도 가능하지만 사전적 정의만으로 처음 듣자마자 저게 뭔 소린지 이해할 수 있는 사람은 많이 없을 것이라 생각한다. 인스턴스는 또 뭐고 객체는 또 무엇인가? 인스턴스는 객체가 실제 코드 상에 선언 된 상태를 얘기한다. 이 인스턴스개 N개 있다고 해도 하나의 Collection 단위의 객체라고 볼 수 있다. (Collection에 대한 이야기는 다음에 하겠다.)

typedef struct unit {
	int level;
    int hp;
    int mp;
    char[20] charName;
} unit;

C언어로 프로그래밍을 처음 배운 사람이라면, 이 녀석이 익숙 할 것이다. 바로 구조체다. 정말 쉽게 이해해보자면 이 구조체가 확장된 개념이라 할 수 있다. 하나의 구조체에는 그 구조체에 할당 된 의미가 존재하는 데이터들이 저장된다.

위의 예시에서는 유닛이라는 구조체 내에 레벨, 체력, 마력, 케릭터 이름이 들어가있다. 우리는 이 구조체로 생성 된 변수 하나하나의 값들을 조정해서 N개의 각각의 특징이 있는 unit 데이터로 활용할 수 있다.

이 구조체를 통해 참 많은 것을 할 수 있다. 포인터 변수를 통해 Linked List와 같은 자료구조도 구현 해 낼 수 있다. 함수 포인터 변수를 집어 넣음으로써 해당 구조체가 특정한 함수를 실행하게 할 수도 있다.

하지만 이것만으로는 현실 세계에 존재하는 하나의 대상에 대한 데이터 처리가 완벽하다고 할 수는 없다.

typedef struct unit {
	int level;
    int hp;
    int mp;
    char[20] charName;
    int (*attack) (struct unit * this); 
} unit;

int attack_function (struct unit * this){
	return 1;
}

int unitConstructor(unit * this){
	this-> attack = attack_function; 
}

int main(){

	unit A;
    unitConstructor(&A);
    int damage = A.attack(&A);
    printf("%d 만큼의 데미지를 입었습니다.\n", damage);
}

구조체 내에 특정한 기능을 집어넣은 예시이다. 하지만 유지보수 하기 상당히 까다로워 보이고 재사용성이 떨어진다. 이것만 가지고 하나의 unit에 대한 데이터를 써 먹을 수는 있겠지만 이 데이터를 기반으로 새로운 무언가를 만들어냄에 있어 복잡한 과정을 거쳐야 한다.

클래스는 이러한 구조체의 한계를 개선하는 데 목적이 있다.

Java Class & Object

클래스를 지원하는 수 많은 언어들이 존재하지만, 자바에서 클래스의 정의, 클래스가 가지는 특징들을 한번 알아보도록 하자.
앞서 언급했듯이 클래스는 구조체의 한계를 개선하는 데 목적이 있다. 클래스 또한 구조체와 마찬가지로 사용자 정의 자료형임에는 이견이 없다. 하지만 구조체에서 함수 포인터를 이용해 특정한 함수를 가져 온 것과 다르게 클래스 자체적으로 메소드를 가질 수 있다. 즉 클래스는 구조체에 비해 좀 더 현실 세계의 Object 그 자체를 투영했다고 할 수 있다.

구조체와 클래스를 구별하는 가장 큰 특징은 상속이라고 생각한다. 구조체는 특정한 타입을 상속해서 또 다른 구조체를 생성해내는 기능이 존재하지 않는다. 최소한 내가 알기로는 말이다.

class chicken {
    private String taste;

    public chicken(String taste){
        this.taste = taste;
    }

    public void eat(){
        System.out.println(taste + " 쿠쿠섬 치킨~");
    }

    public String getTaste(){
        return taste;
    }
}

class spicyChicken extends chicken {
    private String seasoning;
    public spicyChicken(String taste, String seasoning){
        super(taste);
        this.seasoning = seasoning;
    }

    public String getSeasoning(){
        return seasoning;
    }
}

chicken 클래스를 통해 양념치킨 클래스를 만들어 낸 예시다. 이 정도는 어렵지 않다.
헷깔릴까 한번 더 언급하자면 super 는 Parent 클래스의 생성자에 데이터를 집어넣는 역할을 한다. 예시에서 볼 수 있다시피 taste는 Parent 생성자를 참고한다. taste 는 Parent의 필드에 존재하기 때문이다.

상속을 통해 할 수 있는 몇 가지 스킬들이 존재한다. 다음 주제로 넘어가기 전에 좀 더 알아보고 가자.

클래스는 구조체와 마찬가지로 사용자 정의 타입 이라 할 수 있다. 즉 캐스팅이 가능하다.

변수의 데이터타입을 변환(캐스팅) 할 때 규칙이 하나 있다.

int A = 10;
long B = 100;
B = A;

이 경우엔 컴파일러가 에러를 내뱉지 않고

int A = 10;
long B = 100;
A = B;

이 경우엔 에러가 난다. Java 컴파일러는 데이터 타입을 비교했을 때 변환 할 대상이 대입하게 되는 데이터의 타입보다 작다면 자동으로 변환 해 주지만 역은 불가능하기 때문이다.

클래스에서도 비슷한 규칙이 존재한다. 바로 Up Casting

spicyChicken B = new spicyChicken("매운맛", "매운양념");
chicken A = B;
A.eat();
//A.getSeasoning(); 은 에러
//출력 결과는 매운맛 쿠쿠섬 치킨~

양념치킨은 치킨의 child 클래스다. 그러므로 치킨 객체가 생성될 때 굳이 형 변환을 해 줄 필요 없이 그대로 대입해도 성립한다. 이 경우엔 타입이 chicken이 되었으므로 양념치킨 만이 가지고 있던 메소드는 에러처리된다.
반대는 Down Casting 이라고 한다. 아래 코드를 보자.

chicken A = new spicyChicken("존맛", "매운맛");
spicyChicken B = (spicyChicken) A;
B.eat();

치킨 객체를 양념치킨으로 형 변환하여 대입하였다. 이 경우에 선수 조건은 치킨 객체가 양념치킨의 생성자로 생성되어야 한다는 것이다.

이것이 가능한 이유는 다형성 때문이다. 다형성(polymorphism) 이란, 객체가 다양한 타입을 가질 수 있는 특징을 의미한다. 자바에서는 이러한 다형성을 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하여 구현하고 있다.

모든 자바의 클래스들은 암시적으로 Object 객체를 상속받게 된다. Object 클래스는 아래와 같은 기능을 제공한다.

이런 특징을 가지고 있기 때문에 우리는 커스터마이징 한 클래스에서 생성 된 객체들의 데이터를 toStirng 으로 살펴볼 수 있는 것이고 비교 연산을 할 수 있는 것이다.

또한 상속에 규칙이 있는데 다중 상속이 불가능하다는 것이다. 오직 하나의 클래스만 상속 받을 수 있다. Object 클래스는 암시적으로 상속되어 있기 때문에 예외로 한다.

Interface & Abstract Class

public interface Animals extends IBird, IFly {
	public void go();
	public void run();
}

public class Obj implements inter1, inter2 {
     @Override
     ....
}

앞서 말했듯이 Java의 클래스는 다중 상속이 불가능하다. 하지만 마치 다중 상속을 한 것 마냥 사용할 수 있는것이 존재한다. 바로 인터페이스다.
인터페이스와 추상클래스 간의 차이는 면접 주제로 상당히 많이 등장하는 만큼 Java에서 난해한 개념임에는 이견이 없다.

처음에 인터페이스 라는 개념을 공부할 때 어떤 기분이 들었는가? 필자는 개인적으로 이걸 굳이 써야하는 이유가 뭘까 많은 의문이 들었다. 유지보수성이란 개념을 잘 이해하지 못 하던 시절에 주로 이런 생각들을 많이 하였다.

인터페이스는 특정한 행동을 하도록 강제시키는 데 목적이 있다. 클래스가 여러가지가 있겠지만, 어떤 행동을 하게 될 지는 개발자가 어떻게 코딩을 하느냐에 따라 다르다. 하지만 결국 똑같은 결과를 지향하는 메소드들의 규격이 모두 틀리다면 추후 유지보수성에 큰 영향을 끼치게된다.

왜 하필 예시가 시말서인지는...너무 깊게 생각하지 말자. 시말서에 들어가는 각 정보들이 명확히 들어가야 감봉으로 끝나지 않겠는가. 인터페이스와 추상클래스는 이 처럼 클래스에 특정 기능을 반드시 명확하게 구현하게 하는 데 목적이 있다. 인터페이스는 여기서 각 단락들이고, 추상 클래스는 그것들을 모아놓은 하나의 시말서 양식이라 생각하면 편하다.

정말 쉽게 인터페이스를 이해하려면 우리가 왜 문서 양식을 쓰는 지, 특정 제품들의 부품에 왜 표준이 있는 지에 대해 생각해보면 편하다. 당장 작은 볼트 하나만 하더라도 규격이 존재한다. 우리가 사용하는 다양한 제품들에는 이런 규격이 존재하기 때문에 서로 호환이 가능한 경우도 생긴다.

결국 클래스들도 소프트웨어 하나를 이루는 부품이다. 우리는 특정한 클래스를 조금 변형 함으로써 다른 클래스를 생성 해 낼수도 있다. 이런 경우에 해당 클래스가 특정한 기능을 가지도록 하는 게 인터페이스라 할 수 있다. 인터페이스가 존재 함으로써 클래스는 규격을 가진다. 규격을 가지기 때문에 추후 규격이 변경되었을 때 변경해야 할 코드의 수 또한 줄어들게 된다.

추상클래스도 크게 어렵지 않다. 그냥 클래스를 쓰면 되지 왜 추상클래스를 쓰는가? 라는 생각이 들 수 있지만 우리가 PPT 템플릿 하나 가지고 엄청나게 다양한 PPT를 만드는 경험들을 생각해보면 추상클래스의 역할또한 이해 할 수 있다.

단, 추상클래스는 그 자체로는 객체, 즉 인스턴스를 생성할 수 없다. 반드시 그 추상클래스를 바탕으로 새로운 클래스를 작성 해 주어야 한다.

public abstract class SmartPhone {
	
	abstract void display();
	abstract void typing();
	
	public void turnOn() {
		System.out.println("전원을 켭니다.");
	}
	
	public void turnOff() {
		System.out.println("전원을 끕니다.");
	}
}

public class IPhone extends SmartPhone{

	@Override
	void display() {
		System.out.println("Apple!!");
	}

	@Override
	void typing() {
		System.out.println("IPhone typing");
	}

	@Override
	public void turnOff() {
		System.out.println("IPhone turnoff");
	}
}

Outro

자바 팁 연재가 드디어 시작되었다. 하겠다고 예고한지 한참 뒤에서야....쓰게 되었다....ㅠㅠ

다음 주제는 컬렉션으로 돌아오겠다.

profile
블로그 이전합니다 : https://swj-techblog.vercel.app/

0개의 댓글