객체 간의 관계 : 집합, 사용, 상속 관계
객체는 개별적으로 사용되기도 하지만
대부분 다른 객체와 관계를 맺고 있다.
이 관계에는 집합 관계, 사용 관계, 상속 관계가 있다.
집합 관계에 있는 객체는 하나의 부품이고
하나는 완성품에 해당한다.
예를 들어 엔진, 타이어, 핸들, 기어 등으로
구성된 부품들과 완성품인 자동차를
집합 관계로 볼 수 있다.
사용 관계는 객체 간의 상호작용을 말한다.
객체는 다른 객체의 메소드를 호출하여
원하는 결과를 얻어낸다.
예를 들어 사람은 자동차를 사용하므로
사람과 자동차는 사용 관계에 있다고 할 수 있다.
사람은 자동차를 사용할 때 '전진한다', '멈춘다'와
같은 동작에 대한 메소드를 호출한다.
상속 관계는 상위(부모) 객체를 기반으로
하위(자식) 객체를 생성하는 관계를 말한다.
많은 경우 상위 객체는 종류와 같은 큰 카테고리를 의미하고
하위 객체는 구체적인 사물에 해당한다.
예를 들어 기계라는 상위 객체가 있다면,
자동차는 기계의 종류 중 하나이므로 하위 객체라고 할 수있다.
이때 기계와 자동차는 상속 관계에 있다.
이러한 객체 간의 관계를 기반으로 정의해보면,
객체 지향 프로그래밍이란 만들고자 하는
완성품인 객체를 모델링하고, 집합 관계에
있는 부품 객체와 사용 관계에 있는 객체를
하나씩 설계한 후 조립하는 방식으로
프로그램을 개발하는 기법이라 할 수 있다.
그렇다면 구체적으로 어떤 방식과 기준으로
각각의 객체를 설계하고 생성하며 조립하는지
알아보기 위해 객체 지향 프로그래밍의
특징에 대해 알아보자.
객체 지향 프로그래밍의 특징
1. 캡슐화(Encapsulation)
캡슐화란 객체의 필드와 메소드를 하나로 묶고,
실제 구현 내용을 감추는 것을 말한다.
외부 객체는 객체 내부의 구조를 알지 못하며
객체가 노출해서 제공하는 필드와 메소드만
이용할 수 있다. 노출된 메소드와 데이터는 읽거나
쓸 수 있고, 또한 호출될 수 있지만 숨긴 데이터와
메소드는 그렇게 할 수 없다.
이렇게 필드와 메소드를 캡슐화하여 보호하는
이유는 외부의 잘못된 사용으로 인해
객체가 손상되지 않도록 하는 데 있다.
자바(JAVA)에서는 이렇게 캡슐화된 멤버를
노출시킬 것인지, 숨길 것인지 결정하기 위해
접근 제한자(Access Modifier)*를 사용한다.
접근 제한자는 객체의 필드와 메소드의
사용 범위를 제한함으로써 외부로부터
객체와 메소드를 보호한다.
*접근 제한자?
- public
적용 대상 : 클래스, 필드, 생성자, 메소드
접근 가능범위 : 어디서든 접근 가능. 같은 패키지 내에서나 다른 패키지에서도 접근 가능
- protected
적용 대상 : 필드, 생성자, 메소드
접근 가능범위 : 해당 클래스와 하위 클래스에서 접근 가능. 같은 패키지 내에서만 접근 가능
- default
적용 대상 : 클래스, 필드, 생성자, 메소드
접근 가능범위 : 별도로 명시하지 않으면 default에 해당. 같은 패키지 내에서만 접근 가능
- private
적용 대상 : 필드, 생성자, 메소드
접근 가능범위 : 해당 클래스 내에서만 접근 가능. 다른 클래스 접근 불가.
public class Car {
// 상태(속성) - 캡슐화된 멤버 변수(private으로 선언)
private String maker; //제조사명
private String model; //모델명
private int year; //제조년도
private boolean engineStarted; //엔진작동여부
// 생성자
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
this.engineStarted = false; // 초기에는 엔진이 꺼진 상태
}
// 행동(메소드)
// 엔진 시작
public void startEngine() {
if (!engineStarted) {
engineStarted = true;
System.out.println("엔진 작동실행");
} else {
System.out.println("엔진이 이미 작동중");
}
}
// 엔진 정지
public void stopEngine() {
if (engineStarted) {
engineStarted = false;
System.out.println("엔진 정지실행");
} else {
System.out.println("엔진이 이미 꺼진 상태");
}
}
// 상태(속성)에 대한 접근자 메서드(getter)
public String getMake() {
return make;
}
public String getModel() {
return model;
}
public int getYear() {
return year;
}
// engineStarted에 대한 접근자 및 수정자 메서드(getter & setter)
public boolean isEngineStarted() {
return engineStarted;
}
public void setEngineStarted(boolean engineStarted) {
this.engineStarted = engineStarted;
}
}
위의 코드에서는 Car 클래스의 멤버 변수들을
접근 제한자 중 하나인 private으로 선언하여
외부에서 직접 접근하지 못하도록 했다.
대신에 상태에 접근하거나 수정하기 위해
getter와 setter 메서드를 제공하고 있다.
이렇게 함으로써 외부에서는 간단한 인터페이스를
통해 상태를 조작할 수 있지만,
내부 구현은 감춰져 있다.
또 한 가지 중요한 점은, 단일 책임 원칙에 따라
모든 클래스는 각각 하나의 책임만 가져야 한다는 점이다.
클래스는 그 책임을 완전히 캡슐화해야 한다.
2. 추상화(Abstraction)
추상화는 어떤 실체로부터 공통적인 부분이나
관심 있는 특성들만 하나로 모은 것을 의미한다.
어떤 실체를 객체로 프로그래밍할 때
모든 세부 사항에 집중하여 해당 객체를 만들 수
없기 때문에 해당 객체의 중요한 특징들을
추상화하여 표현한다.
추상화를 통해 해당 객체의 복잡성을 다루기 쉽고
이해하기 쉬운 수준으로 단순화할 수 있다.
어떤 하위 클래스들에 존재하는 공통적인
메소드를 인터페이스로 정의하는 것도
추상화에 포함된다.
// 자동차 클래스
public class Car {
private String maker; //제조사명
private String model; //모델명
// 주행 기능
public void drive() {
System.out.println("차량이 주행중입니다.");
}
// 멈춤 기능
public void stop() {
System.out.println("차량이 멈췄습니다.");
}
}
위의 예시에서 Car 클래스는 자동차를 추상화한 것이다.
이 클래스는 자동차의 주요 속성인 제조사와 모델을
나타내는 변수를 필드로 가지고 있고, 주행과 멈춤이라는
기능을 정의하고 있다.
3. 다형성(Polymorphism)
다형성은 같은 타입이지만 실행 결과가
다양한 객체를 이용할 수 있는 성질을 말한다.
코드 측면에서 보면 다형성은 하나의 타입에
여러 객체를 대입함으로써 다양한 기능을
이용할 수 있도록 해준다. 자바에서는 다형성을
위해 부모 클래스 또는 인터페이스의 타입 변환을
허용한다. 부모 타입에는 모든 자식 객체가 대입될
수 있고, 인터페이스 타입에는 모든 구현 객체가
대입될 수 있다.
다형성의 효과로 객체는 부품화(모듈화)가 가능하다.
예를 들어 자동차를 설계할 때 타이어 인터페이스 타입을
적용했다면, 이 인터페이스를 구현한 실제 타이어들은
어떤 것이든 장착(대입)이 가능하다.
// 타이어 클래스
class Tire {
void roll() {
System.out.println("타이어가 굴러갑니다.");
}
}
// 스노우 타이어 클래스(Tire클래스를 상속받음)
class SnowTire extends Tire {
@Override
void roll() {
System.out.println("스노우 타이어가 굴러갑니다.");
}
}
// 자동차 클래스
class Car {
void installTire(Tire tire) {
tire.roll();
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
Car car = new Car();
// 다형성을 이용하여 다양한 타이어 객체를 전달
Tire normalTire = new Tire();
SnowTire snowTire = new SnowTire();
car.installTire(normalTire); // 일반 타이어 장착
car.installTire(snowTire); // 스노우 타이어 장착
}
}
위의 예시에서, 자동차 클래스인 Car의 installTire 메서드는
Tire 클래스의 객체를 매개변수로 받는다.
이 때, 다형성을 이용하여 Tire 클래스를 상속받은
SnowTire 클래스의 객체를 전달할 수 있다.
같은 installTire 메서드가 호출되지만
실제 실행되는 코드는 전달된 객체에 따라
각각 다른 타이어가 장착되므로 다르게 동작하게 된다.
4. 상속(Inheritance)
상속은 상위(부모) 클래스의 멤버(필드, 메서드)를
하위(자식) 클래스에게 물려주는 것을 말한다.
상위 클래스에서 새로운 기능을 추가해 하위 클래스를
만들어 사용하는 기법으로, 하위 클래스에서 따로
정의하지 않아도 상위 클래스에서 정의된 멤버를
자동으로 상속받아 구현할 수 있다.
상속은 이미 잘 개발된 클래스를 재사용해서
새로운 클래스를 만들기 때문에 코드의 중복을
줄여줄 수 있다. (하지만 언제나 그런 것은 아니다.)
상속을 해도 상위 클래스의 모든 필드와 메소드들을
물려받는 것은 아니다. 상위 클래스에서 private 접근 제한을
갖는 필드와 메소드는 상속 대상에서 제외된다.
또한 상위 클래스와 하위 클래스가 다른 패키지에
존재한다면 default 접근 제한을 갖는 필드와 메소드도
상속 대상에서 제외된다.
상위 클래스에서 수정이 발생하면
상속받은 모든 하위 클래스에도 수정 효과를
가져오므로 유지 보수 기간을 줄여줄 수도 있지만
여러 가지 문제점이 발생되기도 하므로
상속이 항상 유용하다고 볼 수는 없다.
// 부모 클래스: Vehicle (차량)
class Vehicle {
String brand;
int year;
void drive() {
System.out.println("차량이 주행합니다.");
}
}
// 자식 클래스: Car (자동차)
class Car extends Vehicle {
int numberOfDoors;
void beep() {
System.out.println("차량이 경적을 울립니다. 빵빵!");
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// Car 객체 생성
Car myCar = new Car();
// Car 클래스에서 상속받은 필드와 메서드 사용
myCar.brand = "Toyota";
myCar.year = 2022;
myCar.numberOfDoors = 4;
System.out.println("브랜드: " + myCar.brand);
System.out.println("연도: " + myCar.year);
System.out.println("차량문수: " + myCar.numberOfDoors);
myCar.drive(); // 상속받은 메서드 호출
myCar.beep(); // 자식 클래스의 메서드 호출
}
}
자바에서는 다중 상속을 허용하지 않기 때문에
하나의 하위 클래스는 하나의 상위 클래스를
상속받을 수 있다.
상위 클래스가 클래스A라면,
클래스B, 클래스C, 클래스D 모두 클래스A로부터
상속받을 수 있지만 클래스B가 클래스A와 클래스E에게
동시에 상속받을 수는 없다. (다중 상속 불가)
대신 아래와 같이 인터페이스를 사용하여
다중 상속을 대체할 수 있다.
아래의 예시를 보면,
클래스 C에서 A와 B 클래스를 동시에 상속받으려고
하면 컴파일 에러가 발생한다.
// 부모 클래스 A
class A {
void methodA() {
System.out.println("부모 클래스 A의 메서드");
}
}
// 부모 클래스 B
class B {
void methodB() {
System.out.println("부모 클래스 B의 메서드");
}
}
// 자식 클래스 C (다중 상속이 불가능)
// class C extends A, B { } // 컴파일 에러!
// 인터페이스 X
interface X {
void methodX();
}
// 인터페이스 Y
interface Y {
void methodY();
}
// 자식 클래스 Z (인터페이스를 이용한 다중 구현)
class Z implements X, Y {
@Override
public void methodX() {
System.out.println("인터페이스 X의 메서드");
}
@Override
public void methodY() {
System.out.println("인터페이스 Y의 메서드");
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// Z 클래스의 객체 생성
Z obj = new Z();
// 다중 구현된 인터페이스의 메서드 호출
obj.methodX();
obj.methodY();
}
}
반면 클래스 Z는 인터페이스 X와 Y를 구현하여,
각각의 메서드를 오버라이딩하여 다중 구현을 구현했다.
이를 통해 클래스 Z는 X와 Y 인터페이스의 모든 메서드를
사용할 수 있게 된다.
'Programming Language > Java' 카테고리의 다른 글
[JAVA] 객체지향 프로그래밍에서의 객체와 메소드 (0) | 2024.02.21 |
---|