본문 바로가기

Java/자료구조

객체지향 프로그래밍과 4가지 특징


 

프로그래밍을 하다 보면 객체, 객체지향이라는 말을 종종 듣게 된다. 이는 프로그래밍 패러다임 중 하나이며, 용어를 그대로 풀어서 쓴다면 객체를 지향하는 프로그래밍을 하라는 뜻이다. 여기서 말하는 객체란 과연 무엇인가? 용어 자체로 이해하는데에는 한계가 있기 때문에 예시를 통해 이해해보도록 하자.

 

 

객체란 하나의 단위를 뜻한다. 우리가 흔히 주변에서 볼 수 있는 사물로는 자동차가 있다.

 

자동차는 각각의 개체로 이루어진 프로그래밍 집합체이다. 자동차에서 핸들, 바퀴, 엔진, 기어 등은 각각의 기능을 가진다.

핸들은 움직이는 방향을 조정하는 기능, 바퀴는 굴러갈 수 있도록 하는 기능. 이와 같은 부품들을 각각 객체라고 부르며 기능들이 모인 집합체가 바로 자동차이다.

 

프로그래밍도 마찬가지이다.

(간단한 예시)

                 바퀴                   코드
public class Wheel {
    private double diameter;
    private double width;
    private string type;
}
                 핸들                  코드
public class Handle {
    private double size;
    private double leftmove;
    private double rightmove;
}

 

프로그래밍에서 객체에 해당하는 코드를 작성할 수 있다. 프로그래밍의 일부분에 해당하는 객체를 먼저 만들고, 각각의 부품(코드)을 조립하여 하나의 완성된 프로그램을 만드는 과정이 바로 객체지향 프로그래밍이다.

 

각각의 객체는 독립적인 기능을 수행하기 때문에 이를 그대로 코드에 반영하여 객체간 결합도를 최대한 낮추는 것이 객체지향 프로그래밍의 핵심이다. 


객체지향 프로그래밍에는 4가지의 기능이 있다.

  • 추상화
  • 상속
  • 다형성
  • 캡슐화

 

추상화(Abstraction)

추상화의 사전적 의미는 "사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것" 이다.

프로그래밍에서는 객체들 사이의 공통적인 기능을 추출하여 인터페이스(Interface)에 따로 정의하는 것을 말한다.

 

이때 인터페이스에서 객체의 역할만 지정해두고 실제 기능은 객체에서 처리하는 것이 바로 추상화이다.

 

예를 들어 강아지와 고양이의 경우를 생각해보자.



 

이 각각의 객체들을 코드로 작성한다면, 아래와 같이 작성할 수 있다.

// 강아지
public class Dog {
    private void eat() {};
    private void sleep() {};
}
// 고양이
public class Cat {
    private void eat() {};
    private void sleep() {};
}

 

코드를 보면, Dog 클래스와 Cat 클래스에서 각각 두 개의 메소드(eat, sleep)가 중복되는 것을 알 수 있다. 현재 예시와 같이 동물이 개와 고양이만 존재할 경우에는 코드를 작성하는데 큰 문제는 없지만, 개와 고양이 뿐만 아니라 사슴, 코끼리 등 다양한 동물을 모두 코드화 할때는 매우 비효율적이다. 

 

따라서 공통된 특성을 인터페이스에 기록하여 기능은 각각의 클래스에서 정의하도록 코드를 작성한다면

public interface Animal {
    public abstract void eat() {};
    void sleep() {}; // public abstract 생략 가능
}
public class Dog implements Animal {
    @override
    public void eat() {'eat meat'};
    
    @override
    public void sleep() {'sleep 10 hours'};
}
public class Cat implements Animal {
    @override
    public void eat() {'eat fish'};
    
    @override
    public void sleep() {'sleep 6 hours'};
}

 

인터페이스를 통해 역할과 구현을 분리하여 작성할 수 있게 되어, 객체 코드만 수정하게 되어 변경이 용이하다.

 

 

상속(Inheritance)

상속은 기존의 클래스의 메소드를 재활용하여 새로운 클래스에서 이용할 수 있는 문법 요소이다. 부모 클래스에서 자식 클래스로 상속이 일어나며, 자식 클래스에서는 상위 클래스의 속성과 기능을 간편하게 사용할 수 있다.

 

또한 오버라이딩(overriding)을 통해 상위 클래스의 메소드를 재정의 하는 것 또한 가능하다.

public class Animal {
    int leg;
    String color;
    
    void eat() {
    	System.out.println('eat');
    }
    
    void sleep() {
    	System.out.println('sleep');
    }
}
// Dog 클래스
public class Dog extends Animal {
    
    void bark() {
    	System.out.println('whal');
    }
}
// Cat 클래스
public class Cat {
	
    @override
    void sleep() {
    	System.out.println('stay up all night');
    }
    
    void cry() {
    	System.out.println('meow');
    }
}
public class Main {
    public static void main(String[] args) {
    	
        // 객체 생성
        Dog dog = new Dog();
        Cat cat = new Cat();
        
        // 객체 속성 정의
        dog.color = "갈색";
        
        // 메소드 실행
        dog.sleep();	// sleep
        cat.sleep();	// stay up all night
    }
}

메인 클래스에서 Dog 또는 Cat 객체를 불러오게 된다면, 각각의 클래스에서 따로 메소드를 지정해주지 않아도 eat(), sleep() 메소드를 호출할 수 있다.

 

 

추상화와 상속

추상화 인터페이스에서 정의된 abstract 메소드가 무조건 하위 클래스에서 정의되어야함
상속 부모 클래스에서 정의된 메소드는 하위 클래스에서 오버라이딩을 통해 재정의를 할 수도 있고, 직접 가져다 쓸 수 있음 

 

 

다형성(Polymorphism)

다형성은 어떤 객체의 속성이나 기능이 상황에 따라 달라지는 것을 의미한다.

 

예를 들어서 어떤 사람은 학교에서는 학생일 수도 있고, 모임에 나가서는 어떤 이의 친구일 수도 있으며, 회사를 다니는 직장인일 수도 있다. 이렇게 객체는 어떤 상황에 놓여있느냐에 따라 속성과 기능이 달라질 수 있다.

 

프로그래밍에서도 마찬가지로 객체가 어떤 상황에 놓이냐에 따라 속성이나 기능이 달라질 수 있다.

 

다형성은 오버로딩(overroading)오버라이딩(overriding)을 통해 구현을 할 수 있다.

 

그리고 다형성은 크게 정적 다형성동적 다형성으로 나눌 수 있다.

 

정적 다형성

정적 다형성은 컴파일이 되는 시점에 결정되는 다형성을 말한다.

오버로딩(overroding)을 통해 구현이 되는데, 같은 이름의 메소드를 파라미터의 타입이나 개수에 따라 여러개를 적용시켜 얻을 수 있는 다형성이다.

 

public class Math {
	// 초기 메서드
	public int plus(int a, int b) {
    	return a + b;
    }
    
    // 파라미터 타입 변경
    public double plus(double a, double b) {
    	return a + b;
    }
    
    // 파라미터 개수 변경
    public add plus(int a, int b, int c) {
    	return a + b + c;
    }
}

 

메인 클래스에서 Math의 add 메소드를 호출 시 적절한 파라미터의 타입이나 개수에 따라 실행되는 메소드가 달라질 수 있다.

 

동적 다형성

동적 다형성은 런타임 시점에서 결정되는 다형성을 말한다.

오버라이딩(overriding), 상속(Inheritance) 또는 인터페이스(Interface)를 통해 구현이 된다.

 

// 상속, 오버라이딩
public class Cat extends Animal{
	
    @override
    void sleep() {
    	System.out.println('stay up all night');
    }
}

 

상속을 통해 부모 클래스의 속성이나 기능을 받을 수 있으며, 오버라이딩을 통해 속성과 기능을 재정의 할 수 있다.

 

// 인터페이스 다형성 사용 전
public class People {
	void make(Cat cat) {
    	cat.eat();
        cat.sleep();
    }
    
    void make(Dog dog) {
    	dog.eat();
        dog.sleep();
    }
    
    void make(Elephant elephant) {
    	elephant.eat();
        elephant.sleep();
    }
    
    // 등등
}

 

위의 코드는 인터페이스 다형성이 적용되지 않은 코드이다. People이 일일히 메소드를 실행시켜야 하기 때문에 코드가 길어지는 문제가 발생한다.

// 인터페이스
public class People {
	void make(Animal animal) {
    	animal.eat();
        animal.sleep();
    }
}

 

모든 동물이 일일히 eat(), sleep() 메소드에 따라 행동하는 코드를 인터페이스를 사용하여 코드 길이를 줄일 수있다.

 

 

캡슐화(Encapsulation)

캡슐화란 서로 연관이 있는 데이터를 하나의 캡슐로 만들어 데이터를 보호하는 것을 말한다.

외부에서 직접적으로 데이터를 접근할 수 없게 만들어서 데이터를 은닉하는 목적을 가진다.

 

접근 제한자(클래스)

객체의 필드를 외부에서 변경하거나 메소드를 호출할 수 없도록 막는 용도로 사용한다.

중요한 필드와 메소드가 외부로 노출되지 않도록 하여 객체의 무결성을 유지할 수 있다.

접근 제한자                 제한 대상                                          제한 범위
public 클래스, 필드, 생성자, 메소드 X
protected 필드, 생성자, 메소드 같은 패키지이거나, 자식 객체만 사용 가능
(default) 클래스, 필드, 생성자, 메소드 같은 패키지
private 필드, 생성자, 메소드 객체 내부

* default는 접근 제한자가 붙지 않은 상태를 말한다.

 

 

접근 제한자(생성자)

객체를 생성할 때에 생성자를 호출할 수 있는 범위를 지정해준다.

접근 제한자              생성자                                                 특징
public 클래스 모든 패키지에서 생성자를 호출할 수 있다.
default 클래스 같은 패키지에서만 생성자를 호출할 수 있다. 
private 클래스 클래스 내부에서만 생성자를 호출할 수 있다.

 

 

접근 제한자(필드, 메소드)

접근 제한자              생성자                                                 특징
public 필드, 메소드 모든 패키지에서 필드를 읽고 변경할 수 있다.( + 메소드 호출)
default 필드, 메소드 같은 패키지에서만 필드를 읽고 변경할 수 있다. ( + 메소드 호출)
private 필드, 메소드 클래스 내부에서만 필드를 읽고 변경할 수 있다. ( + 메소드 호출)

 

 

Getter/Setter

객체의 필드를 외부에서 마음대로 접근할 경우 데이터의 무결성이 깨질 수 있다. 따라서 직접적인 접근을 막고 메소드를 통해 접근을 허용하도록 하는데, 이때 Getter/Setter 메소드를 이용할 수 있다.

 

public class Student {
	// private으로 클래스 내부에서만 접근, 데이터 보호
	private int age;
    private String name;
    
    // getter
    public int getAge() {
    	return age;
    }
    
    public String getName() {
    	return name;
    }
    
    // setter
    public void setAge(int age) {
    	this.age = age;
    }
    
    public void setName(String name) {
    	this.name = name;
    }
    
}