[Java] 객체 지향, 클래스

·2022년 10월 16일

JAVA

목록 보기
5/10

객체 지향 프로그래밍


Java는 객체 지향 언어이다. 객체 지향 프로그래밍은,

  • 코드의 재사용성이 높고(상속, 캡슐화, 다형성),
  • 코드의 관리(유지보수, 업그레이드, 디버깅)가 용이하며,
  • 보안성을 향상할 수 있으며 신뢰성 높은 프로그래밍을 가능하게 한다.

모든 것을 객체와 그 객체들 간의 상호작용으로 보는 것이 객체지향이론의 기본이다. 객체 지향 프로그래밍은 이 이론을 기반으로, 각 속성과 기능을 담은 객체들과 객체들 간의 상호작용의 서술으로 어플리케이션을 작성하는 방식이다. Java를 들여다 보면 모든 것이 클래스에서 시작해서 클래스로 끝난다. 각 클래스에서 각자의 역할을 맡아 세부적인 사항들을 기술하고 있고, 그걸 사용하는 방식-관계-을 잘 서술해내는 것으로 프로그래밍이 이루어진다. 해야할 일을 순차적으로 기술하고 그대로 수행하는 절차 지향 프로그래밍과는 큰 차이가 있다.
대규모의 프로그램은 많은 기능을 포함하기 때문에 객체 지향으로 작성하는 것이 적합하다. 성능(실행 속도) 측면에서는 절차적 프로그램보다 느리다는 단점이 있으나 눈부신 기술의 발전이 있었기에 빠른 속도가 요구되는 분야가 아닌 이상 실생활 어플리케이션 작성에는 아무 문제없이 사용할 수 있다.

 

class


클래스는 객체를 생성하기 위한 템플릿이며 속성과 기능으로 정의되어 있다. 이 클래스라는 틀을 이용해서 객체라는 것들을 찍어낸다.
클래스는 객체 내의 변수와 메서드를 정의해 준다. 객체는 이를 그대로 이어받아 만들어지고, 각자 다른 데이터를 담거나 어떠한 처리를 하는 등으로 사용된다.

class ExClass {
	객체변수;
    메서드() { ... };
    ...
}

...

ExClass ex1 = new ExClass();
// ex1은 객체이자 ExClass의 인스턴스이다.

객체 변수에는 객체명.변수명 으로 접근할 수 있으며,
메서드 역시 객체명.메서드() 으로 접근할 수 있다.
또한 같은 클래스의 객체라도 서로의 값이 (static이 아니라면) 절대 공유되지 않는다. 때문에 객체 지향 프로그래밍이 가능한 것이다.

 

한편 메서드로 하여금 해당 인스턴스의 객체 변수에 접근하기 위해서는

exMethod(String data) {
	this.str = data; // "this."
}

this.변수명 으로 접근할 수 있다.

 


Call by Value? Reference?

메서드에서 '값'으로 받았으면 Value
메서드에서 '객체'로 받아서 '객체 변수'를 건드렸으면 Reference

 


메서드 오버로딩

메서드를 작성하다 보면, 같은 이름의 메서드를 호출하는데 서로 다른 개수나 타입의 매개변수를 전달해야 하는 경우가 발생할 수 있다. 이런 경우에는 그저 각 경우의 수를 모두 기술하면 메서드를 호출할 때 호출한 매개변수의 종류에 맞는 메서드를 알아서 불러오게 된다.

class Eat(){
	void eat(String a) {
    	System.out.println("I ate " + a + " and still hungry.");
    }
    
    void eat(String a, String b) {
    	System.out.println("I ate " + a + " and " + b + " but still hungry.");
    }

	void eat(String a, String b, String c) {
    	System.out.println("I ate " + a + ", " + b + " and " + c ", I'm full!");
    }
}

이렇게 입력항목이 다른 동일한 이름의 메서드를 만들 수 있는데 이를 메서드 오버로딩이라고 한다.

 

상속 (Inheritance)


A 클래스의 기능을 이어받으면서 확장하는 B 클래스를 만들고 싶다면 어떻게 해야 할까?
A 클래스를 복사, 붙여넣기한 뒤 코드를 잇는 대신, '상속' 기능을 쓰면 된다.
extends 키워드를 이용해 클래스를 상속받으면 부모 클래스의 템플릿을 그대로 가져오면서, 추가로 작성한 변수나 메서드를 추가로 갖게 된다.

class Animal {
	String name;
    
    void setName(String name) {
    	this.name = name;
    }
}

class Dog extends Animal { // Animal 상속.
	void bark() {
    	System.out.println(this.name + "barked!");
    }
}

Dog 클래스는 Animal 클래스를 상속하므로 "Dog is a(n) Animal" 이라고 할 수 있다. 이런 관계를 Java에서는 IS-A 관계라고 하며,

Animal dog = new Dog();

와 같이 작성할 수 있다. 하지만 이 경우 객체 dogAnimal의 객체이기 때문에 bark 메서드를 호출할 수 없다.

Dog dog = new Animal(); // 이렇게는 할 수 없다!

한편 자식 클래스에서 부모 클래스에 있는 메서드와 같은 이름과 매개변수의 메서드를 작성하는 것을 메서드 오버라이딩이라고 한다. 자식 클래스의 객체는 오버라이딩된 메서드를 실행한다.

class HouseDog extends Animal { // Dog 상속.
    void bark() { // 메서드 오버라이딩
    	System.out.println(this.name + "barked in house!");
        // HouseDog의 객체에서 bark 메서드를 부르면 이 메서드를 실행한다.
    }
}

- 다중 상속

Java에서는 다중 상속을 지원하지 않는다. 여러 부모 클래스들 중에 같은 이름과 매개변수를 가진 메서드가 있을 경우의 불명확함을 제거하기 위해서이다. (다른 언어의 경우에는 우선순위 적용 등의 방법을 사용한다.)

 

생성자 (Constructor)


생성자는 객체가 생성될 때 호출되는 메서드로, 메서드명이 클래스명과 동일하고 리턴 자료형을 정의하지 않는다.
생성자 역시 메서드처럼 오버로딩이 가능하다. 아래와 같이 작성할 수 있다.

class Animal {
    String name;
    int age;
    
    
    Animal(String name, int age) {
    	this.name = name;
        this.age = age;
    }
     //생성자 내부에서도 생성자 호출이 가능하다. this로 호출한다.
    Animal(String name) { this(name, 10); }
    Animal(int age) { this("dog", age); }
   
}

생성자가 없을 경우에는 아무것도 받지 않고 아무것도 수행하지 않는 디폴트 생성자를 컴파일러가 자동으로 추가한다. 하지만 위와 같이 작성된 경우 디폴트 생성자는 만들어지지 않는다. 따라서 Animal의 객체를 만들 때에는 반드시 name이나 age 혹은 둘 모두를 전달해야만 한다. Animal dog = new Animal(); 처럼은 작성할 수 없다.

 

추상클래스와 인터페이스


위에서 우리는 같은 틀을 가지지만 다른 정보를 담는 객체들을 찍어내기 위해 클래스라는 템플릿을 만들어서 사용했다. 하지만 만약 틀 자체도 약간씩 다른 객체들을 찍어내야 한다면 어떻게 해야 할까?
키보드를 만들어야 한다고 가정해 보자. 납품해야 하는 키보드들은 굉장히 비슷하지만 구조나 기능이 약간씩 다르다. 설계도가 90% 정도는 동일하지만, 10% 정도의 바리에이션이 존재하는 경우이다. 그렇다면 90% 정도의 미완성 설계도를 공유하고, 나머지만 다르게 만들면 되지 않겠는가? 이런 프로그래밍이 필요할 때 우리는 추상클래스 혹은 인터페이스를 사용하게 된다.

추상클래스 (Abstract Class)


추상클래스는 키워드 abstract를 붙여서 만들 수 있고, 객체 생성이 불가능하다. 객체를 생성하려면 반드시 상속을 통해 구현한 클래스부터 생성해야 한다. 추상클래스는 일반 클래스를 상속받아 작성될 수 있다.

abstract class AbsClass {
	abstract String getHello();
    void sayHi() {
    	System.out.println("Hi!");
    }
}

추상클래스는 완성된 부분도 있지만 아무튼 미완성인 설계도와 같다.
클래스의 기능도 갖고 있으면서 아래 기술할 인터페이스의 역할도 갖고 있는 클래스이다.

 

인터페이스 (Interface)


인터페이스를 사용하면 개발시간을 단축할 수 있고, 표준화가 가능하며, 서로 관계없는 클래스들에게 관계를 설정해줄 수 있으며 독립적인 프로그래밍이 가능해진다.
인터페이스는 추상클래스보다 조금 인터페이스의 모든 멤버변수는 public static final이고 생략가능하다.
또한 모든 메서드는 public abstract이고 생략가능하다.
JDK 1.8 (Java 8) 부터는 static 메서드와 default 메서드를 예외로 작성할 수 있다.
디폴트 메서드를 사용하면 구현된 메서드를 작성해둘 수 있고, 스태틱 메서드를 사용하면 일반 클래스의 스태틱 메서드를 사용하는 것과 동일하게 사용할 수 있다.

interface AInterface {
	void aMethod(int a);
}

interface BInterface {
	void bMethod(String str);
}

인터페이스는 인터페이스를 상속받을 수 있고 다중상속이 가능하다.

interface ABInterface extends AInterface, BInterface { }

인터페이스를 구현할 때에는 인터페이스에 정의된 모든 추상 메서드를 구현해야 하고, 일부만 구현할 경우에는 추상 클래스로 선언해야 한다.
또한 상속과 구현을 동시에 할 수도 있다.

class ExClass extends ParentClass implements AVInterface {
	public void aMethod(int a) { 구현; }
    public void bMethod(String str) { 구현; }
}

 

인터페이스에 디폴트 메서드가 추가되면서 추상 클래스와의 차이점이 살짝 모호해졌지만, 추상 클래스는 인터페이스와 달리 객체변수, 생성자, private 메서드를 가질 수 있다.

 

인터페이스는 그 인터페이스를 구현한 클래스가 존재할 때, 매개변수의 타입으로 사용될 수 있다.

	void callInterface(AInterface a) { ...

매개변수의 타입으로 사용된 인터페이스는 해당 인터페이스가 구현된 클래스의 인스턴스를 넘겨준다.
또한 메서드의 리턴타입으로 인터페이스를 지정할 수도 있다. 리턴타입이 인터페이스라는 것은, 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

 

다형성 (Polymorphism)


하나의 객체가 여러 자료형 타입을 가질 수 있는 것을 다형성이라고 한다. 물론, 이는 상속 관계가 존재하기에 가능한 특징이다. 그렇다면 다형성은 어떤 의미를 가질까?
이는 메서드가 실행 시점에 성격이 결정되는 동적 바인딩을 이용해, 상속 관계에 있는 클래스에서 "부모 클래스가 오버라이딩을 통해 자식 클래스의 메서드를 호출할 수 있음"을 의미한다.

부모 클래스는 자식 클래스가 뭘 하는지 알 수 없다. 그럼에도 자식 클래스의 메서드를 가져다 쓸 수 있다!

프로그램의 컴파일 시점에는 부모 클래스는 자신의 멤버 메서드에밖에 접근할 수 없다. 하지만 실행 시점에는 자식 클래스의 메서드를 실행시킬 수 있게 되는 것이다.
다형성은 메소드 오버라이딩이 이루어진 상속 관계에서, 자식 클래스의 객체를 부모 클래스로 업캐스팅했을 때 성립한다. 여러 객체를 하나의 타입으로 관리할 수 있어 코드의 유지보수가 용이해지고, 재사용성이 좋아지며 클래스 간 의존성이 줄어들어 안전성이 높아진다.

// 상속 관계 클래스
class Food {
	public String name;
    Food(String name) { this.name = name; }
    void eat(){
    	System.out.println(name + " 냠냠");
    }
}

class Cookie extends Food {
	public String name;
	Cookie(String name) { this.name = name; }
        void eat(){ // 오버라이드
    	System.out.println(name + " 바삭바삭");
    }
}

...

Food a = new Cookie("빠다코코넛");
a.eat(); // 빠다코코넛 바삭바삭 출력됨

 

 

여러분은 아마도 오늘 읽은 내용으로부터 곧 아래 코드를 보게 될 것이다.

Map<String, String> map = new HashMap<>();

 


이 글은 점프 투 자바자바의 정석을 읽고 스터디한 글입니다.

0개의 댓글