- 객체(Object)를 표현할 때, 흔히, 붕어빵 틀(클래스)에서 찍어낸 붕어빵이라고 표현한다.
- 좀 더 쉽게 아래의 그림으로 보자.
- 위는 강아지를 찍어낼 수 있는 강아지 클래스이다.
- 강아지의 이름, 종, 생일, 좋아하는 간식을 넣을 수 있는 멤버 변수와 명령1, 명령2를 내리는 멤버함수(메서드)로 이루어져 있다.
- 그리고 이 강아지 설계도(클래스)를 가지고 객체를 찍어낼 수 있다.
- 위는 강아지 설계도(클래스)를 이용하여 쫑이를 만든 것이다.
- 이를 코드로 구현하면 다음과 같다
class Dog { //클래스
String name; //이름 ||
String type; //종 || -> 멤버 변수
String birthday;//생일 ||
String favoriteFood; //좋아하는 간식 ||
void giveHand() { System.out.println("오른 손을 내민다.");} //멤버함수(메서드)
void walk() {System.out.println("목줄을 가지고 온다.");} //
}
public class Main() {
public static void(String[] args) {
Dog myDog = new Dog(); //myDog라는 객체를 만든다.
myDog.name = "쫑"; //'.'을 이용하여 멤버 변수와 메서드에 접근한다.
myDog.type = "시츄";
myDog.birthday = "3/4";
myDog.favoriteFood = "개껌";
myDog.giveHand(); //'오른 손을 내민다.'가 출력된다
myDog.walk(); //'목줄을 가지고 온다.'가 출력된다
}
}
- 위와 같이 Dog 클래스를 이용하여 쫑이, 마리, 콩이와 같은 강아지들을 찍어낼 수 있다.
- 객체와 인스턴스 단어의 혼용으로 인해 헷갈려하는 사람들이 많다.
OOP에서 인스턴스는 해당 클래스의 구도로 컴퓨터 저장공간에 할당된 실체를 의미한다.
즉, 객체가 메모리에 할당되어 실제 사용될 때 이를 인스턴스라고 부른다.
다만, 객체지향 프로그래밍을 할 때 객체와 인스턴스 간의 차이를 명확하게 구분하지는 않는다.
- 절차 지향 프로그래밍: 코드를 순차적으로 짜는 것
- 객체 지향 프로그래밍: 코드를 객체 단위로 나누어 구성하여 짜는 것.
절차 지향 | 객체 지향 | |
---|---|---|
장점 | 빠르다 | 생산성이 높다. 코드 재사용성이 높아진다. |
객체를 만들지 않아 메모리 소비가 적다. | 유지보수가 쉽다. | |
코드의 흐름이 보여 매우 직관적이다. | 대형 프로젝트에 적합하다. | |
단점 | 모든 코드 들이 유기적으로 연결 되어 있어 | 절차지향에 비해 느리고, 코드를 작성하기 어렵다. |
코드 수정 및 유지보수가 어렵다. | 객체가 많아지면 소요되는 비용이 커진다. |
- 객체 내부의 어떤 동작에 대한 구현이 어떻게 되었는지 감추어 외부에서 객체를 손상시키는 일을 방지한다.
- 높은 응집도와 낮은 결합도를 목표로 한다.
- 외부에서 접근할 필요 없는 메서드나 변수의 접근 제어자를 private로 설정하여 객체 내부를 공개하지 않음으로써, 의도하지 않은 동작 오류를 최소화한다.
class Dog{
// 접근 제어자는 총 네 가지이다. public, default, private, protected
public String name; // 외부에서 접근 가능
private int age; // 외부에서 접근 불가능
String favoriteFood; // 아무것도 안 지정되어 있을때 default, 동일 패키지 내에서 접근 가능하다.
protected String type; // 동일 패키지이거나, 상속받은 다른 패키지의 클래스에서만 접근 가능하다.
}
public class Main{
public static void main(String[] args) {
Dog myDog = new myDog();
myDog.name = "쫑";
// myDog.age = 10; 접근 제어자가 private이기 때문에 외부에서 접근이 불가능하다
}
}
- 객체들이 공통적으로 필요로 하는 속성이나 동작을 하나로 추출해내는 작업
예를 들어 위의 강아지 클래스를 만든 다고 했을때,
강아지들의 공통적인 특징을 파악한 후 -> 이름, 종, 나이 . . .
하나의 묶음(클래스)으로 만들어 내는 것이 추상화이다.
- 여러 개체들이 지닌 공통된 특성을 하나의 법칙으로 일반화하는 과정이다.
- 예를 들어 학생, 선생님, 학부모, 클래스가 있다고 하자 이들은 모두 사람이라는 공통점이 있다. 이에 따라 아래와 같이 Person이라는 클래스를 만들어 공통된 부분을 물려 받게 할 수 있다.
class Person { // 부모 클래스
String name;
void eat() {System.out.println("밥을 먹는다.");}
void sleep() {System.out.println("잠을 잔다.");}
}
class Student extends Person { // 자식클래스
void eat() {System.out.println("학식을 먹는다.");}
}
class Teacher extends Person { // 자식클래스
void study() {System.out.println("수업을 한다");}
}
😐주의 : 상속을 코드 재사용의 개념으로 이해하면 안 된다!
일반적인 개념을 구체화하는 상황에서 상속을 사용해야 한다.
서로 다른 클래스의 객체가 같은 동작 수행 명령을 받았을 때, 각자의 특성에 맞는 방식으로 동작하는 것이다.
class Car {
void horn() {System.out.println("빵빵");}
}
class 구급차 extends Car {
void horn() {System.out.println("위용위용");}
}
class 경찰차 extends Car {
void horn() {System.out.println("삐용삐용");}
}
class 자전거 extends Car{
void horn() {System.out.println("따르릉"); }}
- 위와 같이 Car클래스를 상속받는 구급차, 경찰차, 자전거가 있을때 아래의 결과가 나온다.
public class Main{
public static void main(String[] args) {
Car car1 = new Car();
구급차 car2 = new 구급차();
경찰차 car3 = new 경찰차();
자전거 car4 = new 자전거();
car1.horn(); // 빵빵
car2.horn(); // 위요위용
car3.horn(); // 삐용삐용
car4.horn(); // 따르릉
}
}
클래스는 단 한 개의 책임을 가져야 한다.
- 한 클래스가 수행할 수 있는 기능이 여러 개라면, 클래스 내부의 함수끼리의 결합이 높아지게되는 문제가 발생한다.
- 예를 들어 어떤 클래스 내에 A라는 메소드가 있고, 이 A메소드는 B메소드를 호출하고 B메소드는 C메소드를 호출한다고 하자. 이때 A의 동작이 일부 수정된다면 B, C메소드를 전부 바꿔야 할 상황이 생기게 되고 이는 매우 비효율적이다. 따라서 이를 모두 분리할 필요가 있다.
- 이에 따라 응집도(cohesion)는 높이고 결합도(coupling)를 낮출 수 있다.
확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다
- 기존의 코드를 변경하지 않고 기능을 수정하거나 추가할 수 있도록 설계해야 한다.
- 어떤 모듈을 수정한다고 하면 그 모듈을 사용하는 다른 모듈 역시 줄줄이 수정해야 할 것이다. 따라서 개방-폐쇄 원칙을 잘 적용하여 기존 코드를 변경하지 않고 새롭게 기능을 만들거나 변경할 수 있도록 해야한다.
- OCP는 추상화(인터페이스)와 상속(다형성)을 통해 구현할 수 있다.
부모 객체를 호출하는 동작에서 자식 객체는 부모 객체의 행위를 수행할 수 있어야 한다.
- 상속 관계가 아닌 클래스들을 상속관계로 설정하면 이 원칙이 위배된다.
위의 Car 클래스를 상속받은 구급차, 경찰차, 자전거 예제를 다시 들고 오겠다.
class Car {void horn() {System.out.println("빵빵");}}
class 구급차 extends Car {void horn() {System.out.println("위용위용");}}
public class Main{
public static void main(String[] args) {
Car car1 = new Car();
구급차 car2 = new 구급차();
car1.horn();//빵빵
car2.horn(); //위요위용
}
}
위는 문제 없이 돌아 간다.
또한 아래의 코드는 정상적으로 작동한다.
public class Main{
public static void main(String[] args) {
Car car3 = new 구급차();
car3.horn(); // 위용위용
}
}
이처럼 자식 객체가 부모 클래스의 타입으로 치환되는 것을 upcasting이라고 한다.
아래의 예제를 집중해서 보자
public class Main{
public static void main(String[] args) {
Car car3 = new 구급차();
// 구급차 car4 = car3; // 에러 발생 car3를 구급차로 형변환 시켜줘야함
구급차 car4 = (구급차)car3; // downcasting
}
}
위에서 처럼, 부모 클래스의 타입을 갖고 있는 객체를 자식타입의 객체로 바꾸고 싶다면 명시적으로 형변환 해주어야 한다. 이를 downcasting이라고 한다.
객체는 자신이 호출하지 않는 메소드에 의존하지 않아야 한다.
- 반드시 필요한 메소드만을 상속/구현한다.
- 상속할 객체의 규모가 너무 크다면 해당 객체의 메소드를 작은 인터페이스로 나눈다.
- 이는 각 객체가 필요한 인터페이스만을 상속하여 구현하면 되므로 각자가 필요한 메소드만을 가지게 된다.
객체는 저수준 모듈모다 고수준 모듈에 의존해야한다.
- 저수준 모듈이 변경되어도 고수준 모듈은 변경이 필요없는 형태가 이상적이다.
- 객체는 객체보다 인터페이스에 의존해야 한다. 즉, 객체의 상속은 인터페이스를 통해 이루어져야 한다.
- 고수준 객체: 인터페이스 등의 추상적인 개념
저수준 객체: 구현된 객체