[JAVA] Object Oriented Programming(OOP) - 추상화, 캡슐화, 상속, 다형성

AI 개발자 웅이·2022년 7월 12일
0

Java

목록 보기
4/11

객체(Object)란 '의사나 행위가 미치는 대상'을 의미하는 것으로 객체 지향 프로그래밍(Object Oriented Programming)은 객체들 간의 상호작용을 기반으로 로직을 구성하는 하는 프로그래밍 방법이다. Java는 대표적인 객체 지향 프로그래밍 언어이다.

객체 지향 프로그래밍 과정을 3가지로 정리하면 아래와 같다.

  1. 객체를 정의한다.
  2. 객체의 기능을 구현한다.
  3. 객체 사이의 상호작용을 구현한다.

예를 들어, 학생이 학교까지 가는 과정을 객체 지향 프로그래밍 방식으로 간단하게 도식화를 한다면 아래와 같이 정리할 수 있다.

객체 지향 프로그래밍의 장단점

장점

  • 코드 재사용이 용이함
    - 상속을 통해 코드 재사용을 할 수 있고 남이 만든 클래스를 쉽게 이용할 수 있다.

  • 생산성이 향상됨
    - 클래스 단위로 모듈화하고 독립적인 객체를 생성하는 방식이기 때문에 공동 작업에서 생산성이 향상된다. 특히 많은 개발자가 작업하는 대형 프로젝트를 진행할 때 업무 분담을 쉽게 할 수 있다.

  • 유지보수가 쉬움
    - 프로그램 추가, 수정 시 캡슐화를 통해 주변에 미치는 영향을 제한할 수 있으므로 (절차 지향 프로그래밍에 비해) 작업을 쉽게 진행할 수 있다.

단점

  • 실행 속도가 느림
    - 객체 지향 프로그래밍은 캡슐화와 격리 구조 때문에 (절차 지향 프로그래밍에 비해) 실행 속도가 느리다.

  • 상대적으로 큰 용량이 필요함

  • 개발 속도가 느림
    - 객체가 처리하려는 것에 대해 정확하게 파악해야 하므로, 설계 단계에서 많은 시간과 노력이 필요하다.

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

객체 지향 프로그래밍의 가장 큰 특징은 객체의 속성(attribute, property, member variable)과 기능(method, member function)을 정의한 클래스(class)를 토대로 실제 메모리에 객체(instance)를 생성하여 객체간의 상호작용을 코드로 구현한다는 점이다. 이때 클래스는 객체에 대한 청사진(blueprint) 역할을 한다.

예를 들어, 학생 클래스를 정의할 때 속성에는 학번, 이름, 학년 등의 정보가 포함되고 기능에는 수업 듣기, 시험 보기 등의 정보가 포함된다. 이때 학생 클래스를 토대로 생성한 학생 객체로는 '수업 듣기, 시험 보기 등의 행위를 할 수 있는 1학년, 20220001 학번의 웅이'가 생성될 수 있다.

객체 지향 프로그래밍은 이 외에도 추상화, 캡슐화, 상속, 다형성의 네 가지 큰 특징을 지닌다.

추상화(abstraction)

추상화란 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 말한다. 객체들은 공통된 클래스를 토대로 생성되는데, 이때 객체들이 어떤 공통된 특징을 가지고 있어야 한다고 정의하는 것이다. 즉, 추상화는 객체들의 공통된 특징을 파악하여 정의해놓은 설계 방법이라고 볼 수 있다. 이때 추상(abstract) 클래스를 상속(inheritance)하거나 interface를 구현(implements)하는 방식으로 추상화를 실현하는 경우가 많다.

아래는 추상화 예제이다.

public abstract class Car {

	public abstract void drive();

	public abstract void stop();

	public void startCar() {
		System.out.println("시동을 켭니다.");
	}

	public void turnOff() {
		System.out.println("시동을 끕니다.");
	}

	// 템플릿 메서드
	final public void run() {
		startCar();
		drive();
		stop();
		turnOff();
	}
}
public class ManualCar extends Car {
	@Override
	public void drive() {
		System.out.println("사람이 운전합니다.");

	}

	@Override
	public void stop() {
		System.out.println("사람이 자동차를 멈춥니다.");
	}
}
public class AICar extends Car {

	@Override
	public void drive() {
		System.out.println("자율 주행합니다.");

	}

	@Override
	public void stop() {
		System.out.println("자동차가 스스로 멈춥니다.");

	}
}

자식 클래스인 ManualCar와 AICar에서 구현이 되어야 하는 공통 기능인 drive, stop이라는 메서드를 부모 추상 클래스인 Car에서 추상 메서드 형태로 설계하고 자식 클래스에서 overriding하는 형태로 추상화를 실현할 수 있다. 추상 클래스와 interface 사용 외에도 일반적인 클래스-인스턴스 관계를 추상화가 실현된 것라고 생각할 수 있다. 기반 클래스가 같은 객체들간에 정의되어야 하는 공통 속성을 클래스에서 변수로 정의하고 공통 기능을 메서드로 정의하는 것 또한 추상화이기 때문이다.

캡슐화(encapsulation)

캡슐화란 외부에서 알 필요가 없거나 숨겨야 하는 부분을 감춤으로써 대상을 캡슐 형태로 단순화하는 것이다. 캡슐화가 중요한 이유는 불안정한 부분(implementation)과 안정적인 부분(public interface)를 분리하여 관리하기 때문에 변경의 여파를 줄일 수 있고 불필요한 정보를 은닉할 수 있기 때문이다.

아래는 캡슐화 예제이다.

public class UserService {

	private final UserRepository userRepository;
	private final PasswordEncoder passwordEncoder;

	public void addUser(final String email, final String pw) {
		final String encryptedPassword = passwordEncoder.encryptPassword(pw);

		final User user = User.builder()
				.email(email)
				.pw(encryptedPassword).build();

		userRepository.save(user);
	}
}

위 예제는 사용자의 이메일과 비밀번호를 입력받아 암호화한 후 DB로 저장하는 코드이다. 새로운 사용자를 추가하는 UserService 입장에서 중요한 것은 email과 pw를 입력받으면 암호화된 정보가 DB에 저장된다는 것이지 구체적으로 암호화 알고리즘을 알 필요가 없다. 따라서 UserService 입장에서는 addUser는 객체 외부에 public interface로 공개하고, 구체적인 암호화 알고리즘은 객체 내부에 implementation으로 숨겨둔 것이다. 객체 내부에 implementation으로 숨겨야하는 것은 변경될 수 있거나 알 필요가 없는 모든 것들이며, 객체의 상태는 숨기고 행동만 외부에 공개해야 한다. 즉, 클라이언트 입장에서 객체에 무언가 요청하는 것은 public interface로 공개하고 알 필요가 없는 세부 알고리즘은 implementation으로 숨겨두는 것이 좋은 캡슐화 방식이다.


자바의 접근 제어자

public : 접근 제한 없음
protected: 동일한 패키지 내에 존재하거나 하위 클래스에서만 접근 가능
default : 아무런 접근 제한자를 명시하지 않으면 default 값이 되며, 동일한 패키지 내에서만 접근 가능
private: 자기 자신의 클래스 내에서만 접근 가능


상속(inheritance)

상속이란 기존 상위클래스의 속성과 기능을 가져와 하위 클래스에서 재사용하는 것을 의미한다. 상속은 코드의 재사용으로 인해 유지 보수를 용이하게 한다는 점에서 중요한 특징이다. 흔히 상속은 is-a 관계라고도 불린다.

아래는 상속 예제이다.

public class Customer {

	protected int customerID;
	protected String customerName;
	protected String customerGrade;
	int bonusPoint;
	protected double bonusRatio;

	public Customer(int customerID, String customerName) {
		this.customerID = customerID;
		this.customerName = customerName;
		this.customerGrade = "SILVER";
		bonusRatio = 0.01;
	}


	public int calcPrice(int price) {
		bonusPoint += price * bonusRatio;
		return price;
	}

	public String showCustomerInfo() {
		return customerName + " 님의 등급은 " + customerGrade + "이며, 보너스 포인트는 " + bonusPoint + "입니다.";
	}
}
public class GoldCustomer extends Customer {

	public GoldCustomer(int customerID, String customerName) {
		bonusRatio = 0.05;
	}

}
public class VIPCustomer extends Customer {

	private int agentID;
	private double saleRatio;

	public VIPCustomer(int customerID, String customerName, int agentID) {
		customerGrade = "VIP";
		bonusRatio = 0.05;
		saleRatio = 0.1;
		this.agentID = agentID;
	}

	public int getAgentID() {
		return agentID;
	}

	public int calcPrice(int price) {
		bonusPoint += price * bonusRatio;
		return price - (int) (price * saleRatio);
	}

}

위의 예제에서 GoldCustomer와 VIPCustomer는 Customer 클래스를 상속받고 있어서, Customer의 변수와 메서드를 재사용하고 있다. 하위 클래스에서 final이 아닌 메서드에 대해 overriding 또한 가능하다. 이처럼 상속은 코드의 재사용을 가능하게 한다는 특징이 있다. 하지만, 상속의 여러가지 치명적인 단점들 때문에 코드의 재사용을 위한 다른 방법인 합성(composition, has-a 관계)을 사용하는 것이 권장된다고 한다.

상속의 단점 및 한계점
1. 캡슐화가 깨지고 결합도가 높아짐
2. 유연성 및 확장성이 떨어짐
3. 다중상속에 의한 문제가 발생할 수 있음
4. 클래스 폭팔 문제가 발생할 수 있음

자세한 내용은 아래 링크에서 확인할 수 있다.

코드의 재사용, 상속보다는 합성을 사용해야 하는 이유

다형성(polymorphism)

위키피디아에서는 다형성을 다음과 같이 정의한다.

프로그램 언어의 다형성은 그 프로그래밍 언어의 자료형 체계의 성질을 나타내는 것으로, 프로그램 언어의 각 요소들(상수, 변수, 식, 오브젝트, 함수, 메소드 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다. 반댓말은 단형성으로, 프로그램 언어의 각 요소가 한가지 형태만 가지는 성질을 가리킨다.

쉽게 말해, 다형성이란 하나의 객체에 여러 타입을 대입할 수 있는 성질을 의미한다. 다형성을 구현하는 대표적인 방식에는 오버로딩(overloading)과 오버라이딩(overriding)이 있다. 하나씩 예제와 함께 살펴보자.

오버로딩(overloading)

오버로딩은 하나의 클래스 안에서 같은 이름의 메서드(또는 생성자)를 사용하지만 각 메서드마다 다른 기능을 하는 것을 의미한다. 오버로딩을 통해 다형성을 구현하려면 특정 규칙을 지켜야 한다.

  1. 메소드의 이름이 같아야 한다.
  2. 매개 변수의 개수 또는 타입이 달라야 한다.
  3. 매개 변수는 같고, 리턴 타입이 다를 때는 성립하지 않는다.
  4. 오버로딩된 메소드들은 매개 변수로만 구분될 수 있다.

아래는 오버로딩 예제이다.

public class toStringValue {
    public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }
  
    public static String valueOf(boolean b) {
        return b ? "true" : "false";
    }

    public static String valueOf(char c) {
        if (COMPACT_STRINGS && StringLatin1.canEncode(c)) {
            return new String(StringLatin1.toBytes(c), LATIN1);
        }
        return new String(StringUTF16.toBytes(c), UTF16);
    }
    
    public static String valueOf(int i) {
        return Integer.toString(i);
    }

    public static String valueOf(long l) {
        return Long.toString(l);
    }

    public static String valueOf(float f) {
        return Float.toString(f);
    }

    public static String valueOf(double d) {
        return Double.toString(d);
    }
}

위의 코드처럼 앞서 언급한 네가지 규칙을 지킨다면 같은 이름의 메서드를 정의할 수 있고 아래와 같은 형식으로 사용이 가능하다.

//숫자를 문자열로 바꾸는 경우
String age = stringValue(3.3);

//boolean을 문자열로 바꾸는 경우
String today = stringValue(true);

또한, 생성자도 오버로딩이 가능하다.

public class Customer {

	private int customerID;
	private String customerName;

	public Customer() {
		this.customerID = 1;
		this.customerName = "이름 없음";
	}

	public Customer(int customerID, String customerName) {
		this.customerID = customerID;
		this.customerName = customerName;
	}
}

오버라이딩(overriding)

메소드 오버라이딩은 상위 클래스의 메소드를 재정의하는 것을 의미한다. 주로, 클래스 상속이나 인터페이스 상속에서 많이 사용되는 방법이다.

아래는 오버라이딩 예제이다.

public class Animal {
	public void move() {
		System.out.println("동물이 움직입니다.");
	}
}

public class Human extends Animal {

	@Overriding
	public void move() {
		System.out.println("사람이 두 발로 걷습니다.");
	}
}

public class Tiger extends Animal {

	@Overriding
	public void move() {
		System.out.println("호랑이가 네 발로 뜁니다.");
	}
}
public class AnimalTest {

	public static void main(String[] args) {
		AnimalTest test = new AnimalTest();
		test.moveAnimal(new Human());
		test.moveAnimal(new Tiger());
	}

	public void moveAnimal(Animal animal) {
		animal.move();
	}
}

위 예제에서는 Animal 클래스를 상속받은 Human, Tiger 클래스에서 move라는 메서드를 재정의한다. 이후 또다른 메서드에서 Animal 클래스로 다운캐스팅 후 같은 move 메서드를 사용하는데 다른 결괏값이 도출되는 것을 확인할 수 있다.

참고 사이트

객체 지향 프로그래밍의 5가지 설계 원칙: SOLID

코드의 재사용, 상속보다는 합성을 사용해야 하는 이유

profile
저는 AI 개발자 '웅'입니다. AI 연구 및 개발 관련 잡다한 내용을 다룹니다 :)

0개의 댓글