본문 바로가기

객체지향프로그래밍

객체지향 프로그래밍의 특징을 알아보자

SOLID와 OOD를 정확하게 알기 위해 틈틈히  공부를 하고 있는데 곰곰이 생각을 해보니 OOD와 SOLID가 연관이 있고 OOD를 각각 섞을 때마다 SOLID의 원칙이 하나씩 나오는 것 처럼 느껴졌습니다.

 

그래서 SOLID를 하기 전 OOD의 정확한 특징을 알아보고 그것을 활용하는 방법을 알게 된 뒤 SOLID를 기술하는 포스트를 적으려고 합니다.

 

객체지향 프로그래밍의 특징


객체지향 프로그래밍의 특징으로는  추상화, 상속, 다형성, 캡슐화  4가지가 있습니다.

이러한 특징들을 활용하여 객체들이 유기적으로 상호작용함으로써 하나의 기능을 완성하게 되는데, 이는 어떤 한 클래스나 객체가 변경되거나 오류가 발생했을 때 유지보수나 오류 해결을 보다 쉽게 만들어줍니다. 

각 객체는 자체적으로 정의된 역할을 수행하며, 객체 간의 상호작용은 외부에서는 내부의 세부 사항을 알 필요 없이 각 객체의 공개된 인터페이스를 통해 이루어지기 때문에 코드의 모듈성이 증가하고, 변경이 발생해도 다른 객체에 영향을 미치지 않도록 하는데 기여합니다.

 

 

1. 추상화

추상화는 제가 가장 좋아하는 개념입니다. 

프로그래밍이라는 것은 결국 현실 세계의 개념을 컴퓨터가 이해하고 처리할 수 있는 형태로 표현하는 과정인데, 추상화는 표현하는 과정에서 아주 중요하고 핵심적인 부분만 추려 특징을 강조하는 역할을 담당하고 있습니다. 

그렇기에 추상화는 개발자가 가져오고 싶은 현실 세계의 개념에 대해서 아주 명확한 특징을 통해 높은 기대치를 가집니다.

추상화의 특징으로는 단순화와 숨김, 일반화가 있는데 이 것을 복잡하고 어려운 단어로 멋있게 설명을 할 수도 있습니다.

그러나 저는 공통적이자 핵심적인 것들에 초점을 맞추고 그 외의 것들은 부수적인 것들로 생각한다고 표현을 하고 싶습니다.  

추상화의 구현 방식은 3가지로 나뉘는데, 

1. 클래스와 객체
2. 인터페이스와 추상 클래스
3. 데이터 추상화

이렇게 3가지입니다.

클래스와 객체는 클래스와 객체는 클래스가 객체를 생성하기 위한 설계도이며, 객체는 해당 클래스의 실제 인스턴스입니다. 따라서 이를 통해 현실 세계의 개념을 모델링하고 필요한 특징을 추상화합니다

인터페이스와 추상 클래스는 추상화한 개념들을 인터페이스에서 표현하고 그것을 상속받은 서브 클래스에서 실현하는 과정을 말합니다.

데이터 추상화는 모델링 단계에서 '데이터'에 집중하여, 데이터의 핵심 특징을 추상화하고 효율적인 데이터 구조를 설계하는 프로세스입니다.

추상화의 특징적인 장점으로는 복잡성 감소와 설계의 명확성이 있는데 현실 세계의 개념을 옮겨올 때 필요한 부분에만 초점을 맞추어 명확하고 이해하기 쉽게 만드는 것입니다.

 

abstract class Shape {
    // Shape를 추상 클래스로 만들고 모양을 그린다는 추상 메서드 설정 
    abstract void draw();
}


class Circle extends Shape {
	// Shape를 상속받고 추상 클래스 내의 구현되지 못한 추상 메서드를 구현하는 과정 
    void draw() {
        System.out.println("원을 그립니다.");		
    }
}

class Rectangle extends Shape {
    void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

public class AbstractionExample {
    public static void main(String[] args) {
        Shape circle = new Circle();
        Shape rectangle = new Rectangle();

        circle.draw(); // 원을 그립니다.가 출력되므로 같은 것을 상속받아서 @Override도 안했는데
        rectangle.draw(); // 각자 다른 값이 출력된다!!
    }
}

 

이렇게 추상 메소드는 추상 클래스를 상속받은 클래스에서 반드시 구현되야하는 특징을 가지고 있다.

 

같은 추상 클래스를 각자 다른 클래스에서 상속받아 각각 다르게 추상 메서드를 구현하는 순간 다른 값을 출력해낸다.

 

아주아주 아름답고 훌륭한 특징이라고 생각한다.



2. 상속

상속은 부모(슈퍼) 클래스의 필드와 메서드를 자식(서브) 클래스가 물려받아 "재사용"할 수 있도록 해주는 개념입니다.

즉, 상속은 객체지향 프로그래밍의 장점 중 하나인 코드의 재사용성을 부각시키는 장점을 가지고 있습니다. 이 외에도 계층 구조를 형성한다는 장점도 있는데, 최초의 가장 부모 클래스를 '더' 추상적으로 만들고 '더' 구체적인 실현단계로 만드는 자식 클래스로 나아가는 계층 구조를 말합니다.

아래는 상속의 예시 코드입니다.

class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " speaks");
    }
}

class Monkey extends Animal {
    public Monkey(String name) {
        super(name);
    }

    @Override
    public void speak() {
        System.out.println(name + " Ukkkii");
    }
}

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void speak() {
        System.out.println(name + " Yaong");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal genericAnimal = new Animal("Generic");
        genericAnimal.speak();

        Monkey monkey = new Monkey("khaha");
        monkey.speak();

        Cat myCat = new Cat("navi");
        myCat.speak();
}


위의 상속 예시 코드를 보면 @Override라는 것이 눈에 띕니다. 

오버라이딩은 다형성의 개념도 가지고 있습니다. 이는 부모 클래스의 메서드를 필요에 따라 다시 정의하기 위한 어노테이션(@)입니다. 

오버라이딩은 상속이 진행된 후에 가능하므로 상속의 장점으로 분류했습니다. 

생각하기에 따라서 아주 쉽고 간결하면서 활용도가 높아 보이지만 주의해야 할 사항도 적지 않습니다. 

객체지향 언어에서는 대체로 단일 상속을 지향하는데, 이는 다중 상속을 할 경우 코드의 가독성을 떨어뜨리고 충돌이 발생했을 때 어떤 부모 클래스의 메서드를 사용한 것인지 애매해질 수 있다는 점입니다. 

또한 자식 클래스는 부모 클래스에 종속되기 때문에 부모 클래스가 수정이 자식 클래스에게는 치명적인 영향을 미칠 수 있으므로 설계 단계에서 명확하게 구분을 지어주어야 합니다.

3. 다형성

다형성은 동일한 인터페이스를 사용하여 "유연하게" 여러 타입의 객체를 다루는 개념을 가리킵니다.

다형성은 크게 정적 다형성과 동적 다형성으로 나뉩니다. 프로그래밍 언어에서 정적은 Static, 동적은 Dynamic이라는 단어를 사용하는데, 자주 등장하니 한 번 알아두면 다음에는 더 쉽게 접근할 수 있을 것입니다.

3 - 1. 정적 다형성

정적 다형성은 메서드 오버로딩을 통해 구현됩니다. 

오버로딩은 같은 이름의 메서드가 각기 다른 매개변수의 타입이나 개수를 가지고 있을 때 다르게 정의되는 것을 의미합니다. 

이를 통해 메서드의 이름이 동일하면서도 다양한 상황에 맞게 사용이 될 수 있는데, 자료형의 형태만 바꿔도 사용할 수 있는 경우의 수가 늘어나고 이에 코드의 재사용성이 올라가는 효과를 볼 수 있습니다.

특히 오버로딩과 오버라이딩이 비슷한 단어라 혼란을 주는 경우가 있습니다. 

이번 기회에 잘 구분하면 좋을 것입니다.



3 - 3. 동적 다형성

동적 다형성은 메서드 오버라이딩을 통해 구현됩니다. 부모 클래스에게서 상속받은 자식 클래스가 부모 클래스가 가지고 있는 메서드를 재정의(오버라이딩)해서 사용하는 것과 인터페이스나 추상 클래스를 다형성을 통해 구체화하는 경우도 있습니다.

아래는 다형성의 예시 코드입니다.

class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public void speak() {
        System.out.println(name + " speaks");
    }
}

class Monkey extends Animal {
    public Monkey(String name) {
        super(name);
    }

    @Override
    public void speak() {
        System.out.println(name + " Ukkkii");
    }
}

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void speak() {
        System.out.println(name + " Yaong");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal genericAnimal = new Animal("Generic");
        genericAnimal.speak();

        Monkey monkey = new Monkey("khaha");
        monkey.speak();

        Cat myCat = new Cat("navi");
        myCat.speak();
    }
}


위의 상속 예시 코드를 그대로 가져왔습니다. 

클래스 Animal은 public void speak()라는 메서드를 가지고 있습니다. 

이 메서드는 매개변수를 사용해서 넣은 name 값에 "speaks"라는 출력을 하는 메서드입니다. 

Animal을 상속받은 Monkey 클래스를 보면 여기에도 public void speak()라는 메서드가 있지만 이 메서드는 name 값에 "Ukkkii"라는 값이 출력될 것입니다. 

부모 클래스에서 상속받아 똑같은 메서드이지만 자식 클래스에서 필요에 따라 재정의를 할 수 있다는 것은 큰 메리트입니다. 

다만, 다형성은 주로 상속을 통해서 구현되기 때문에 적절한 상속 구조와 계층 구조의 중요도가 강합니다. 

또한 다형성은 인터페이스를 통해 활용하는 것이 일반적이고 유용하기 때문에 설계 단계를 아주 신중하게 해야 합니다.

4. 캡슐화

캡슐화는 필드와 메서드를 하나의 단위로 묶어 객체의 내부 를 숨기고, 외부에서는 접근 제한자를 통해 원하는 필드나 메서드만 접근할 수 있게 하는 개념입니다. 

접근 제한자는 캡슐화의 핵심 요소 중 하나로, 객체의 내부 구현 세부 사항을 외부에 숨겨서 외부에서는 오직 접근 제한자를 통해 명시된 필드나 메서드만 접근할 수 있도록 하는 역할을 합니다. 

이로써 객체의 상태를 안전하게 유지하고 코드의 유지보수성을 높일 수 있습니다.

 

아래는 캡슐화의 예시 코드입니다.

public class Car{

// 필드를 private로 선언하여 캡슐화 (외부에서 직접적으로 필드 값에 영향을 줄 수 없게 하기 위함)
private int speed;
private String color;

public int getSpeed(){
	return speed;			//Getter메서드를 넣어주는 이유는 외부에서 필드 값에 접근하기 위함
}

public String getColor(){
	return color;
}

public void setSpeed(int newSpeed){		// Setter메서드를 통해서 필드의 값을 설정하기 위함
	if(newSpeed >= 0){ 		// Setter메서드에서 유효성을 검사하면서 오류를 줄일 수 있다.
    	speed = newSpeed;
    }
}
	
public void setColor(String newColor) {
   color = newColor;
}

public void accelerate() {		// 동작을 의미하는 메서드도 사용가능
    speed += 10;
}

public void brake() {
    speed -= 5;
    if (speed < 0) {
        speed = 0;
    }
}

public static void main(String[] args){
	
    Car kia = new Car(); 	//객체를 인스턴스로 만들기
    
    kia.setSpeed(100); 		//Setter로 값 변화주기
    kia.setColor(black);
    
    System.out.println("Speed: " + kia.getSpeed()); 	// 현재 차의 필드값이 얼마인지 Getter로 읽기
    System.out.println("Color: " + kia.getColor());
    
    kia. accelerate(); 		// 동작을 의미하는 메소드로 넣은 메서드 실행
    System.out.println("Speed: " + kia.getSpeed()); 	// Getter로 동작이 실행된 뒤의 값 읽기
    kia.brake();
    System.out.println("Speed: " + kia.getSpeed());

 

이렇게 캡슐화는 객체지향 프로그래밍의 핵심 원리 중 하나로, 객체의 내부를 숨기고 필요한 만큼만 외부에 접근을 허가해주어 객체의 세부 사항을 안전하게 숨기고 접근 제한자를 통해 다른 객체와 상호 작용 할 수 있습니다.

 

캡슐화를 통해 작성된 코드는 객체 간의 안전하고 견고한 상호 작용을 가능하게 합니다.

 

 

이번 포스트에서는 객체지향 프로그래밍의 특징 (OOD)에 대해서 알아봤습니다. 

 

다음 포스트에서는 객체지향 설계 원칙(SOLID)에 대해서 알아보겠습니다.