티스토리 뷰
객체 지향 프로그래밍(Object Oriented Programming) 특징과 클래스(Class),인스턴스(Instance),객체(Object)의 정의
Ji@n 2024. 7. 30. 12:14
과거에 실습을 하다 배운 내용을 이해하고자 예시를 들어가며
설명했는데.. 다시 보니
내가 봐도 이해가 잘 안가서 공부해본걸 다시 기록하려고 한다.
2024.02.07 - [2월] - 2024.02.07_ 객체 지향 프로그래밍 , 메소드 , 클래스 (+ 객체, 인스턴스)(수정)
자바의 특징 중 하나를 설명하라고 하면 가장 먼저 떠오르는 단어는
객체지향프로그래밍 (Object Oriented Programming)
먼저 객체지향프로그래밍의 정의를 설명하자면 "객체를 먼저 만들고 하나씩 조립하여 완성된 프로그램을 만드는 기법" 이다.
객체를 이해하기 위해 쉬운 예시로 카페에 비유해서 설명해보겠다. (+ 클래스, 인스턴스, 메소드, 속성)
메뉴판에 있는 아메리카노를 주문하는 예시로 들면
메뉴판(= 여러 클래스의 집합)의 각각의 음료(ex: 아메리카노, 라떼 등)는 개별적인 클래스로 생각할 수 있다.
이 클래스들은 컵의 사이즈, 커피 양, 가격 등의 속성(attributes)과 준비 방법, 서빙 방법 등의 행동(methods)을 정의한다.
이중 '아메리카노' 클래스는 컵의 사이즈, 커피 양, 가격 등의 속성과 준비 방법, 서빙 방법 등의 행동(메소드)을 정의한다.
주문 넣은 아메리카노 = 인스턴스, 이 인스턴스는 구체적으로 어떤 컵 사이즈와 커피 양, 맛 등을 갖는지 정의된 상태이다.
아직 주문만 들어갔지.. 실제로 만들어지지 않았다.
실체가 있는 아메리카노 = 객체, 주문을 받고 실제로 만들어진 '아메리카노'가 완성된 객체이다.
이 객체는 '아메리카노' 클래스의 인스턴스이며, 물리적인 실체를 가지고 있다.
클래스의 정의를 바탕으로 실체화 된 것이 객체이다.
정리하자면,
클래스 = '설계도' or '청사진'
인스턴스 = 설계도에 따라 만들어진 구체적인 객체의 상태
객체 = 실직적으로 존재하는 것, 즉 물리적인 실체가 있는 것이다.
결국 객체지향프로그래밍은
클래스를 설계하고 객체(인스턴스)를 생성하여, 생성된 객체들의 상호작용을 통해 프로그램의 기능을 구축하는 방법
이다.
구체적인 과정 설명
객체지향 프로그래밍 (OOP, Object-Oriented Programming) 기법을 구체적으로 설명하면
첫 번째 단계는 클래스를 설계하는 것이다.
클래스는 프로그램에서 사용할 객체의 타입과 해당 객체가 가질 속성(데이터)과 행동(메소드)을 정의한다.
이를 통해 프로그램에서 사용할 객체의 구조와 동작 방식을 미리 정의한다.
설계된 클래스에 따라 객체(인스턴스)를 생성한다. 객체는 클래스의 속성 값을 가지며, 클래스에서 정의된 메소드를 통해 특정 행동을 수행할 수 있다.
생성된 객체들은 프로그램 내에서 상호작용한다.
이 때, 객체들은 메시지를 주고받거나 메소드를 호출하며 데이터를 교환한다.
객체들 간의 상호작용을 통해 프로그램의 기능이 구현된다.
객체들이 조립되고 상호작용하면서 프로그램의 전체 기능이 완성된다.
객체지향 프로그래밍에서는 이러한 과정을 통해 프로그램을 모듈화하고, 유지보수와 확장성을 높인다.
// 클래스 정의
class Americano {
// 속성 (Attributes)
private String size;
private int coffeeAmount;
private double price;
// 생성자 (Constructor)
public Americano(String size, int coffeeAmount, double price) {
this.size = size;
this.coffeeAmount = coffeeAmount;
this.price = price;
}
// 행동 (Methods)
public void prepare() {
System.out.println("Preparing an Americano...");
System.out.println("Size: " + size);
System.out.println("Coffee Amount: " + coffeeAmount + "ml");
}
public void serve() {
System.out.println("Serving an Americano for $" + price);
}
// 속성에 대한 접근자와 설정자 (Getters and Setters)
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public int getCoffeeAmount() {
return coffeeAmount;
}
public void setCoffeeAmount(int coffeeAmount) {
this.coffeeAmount = coffeeAmount;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
// 메인 클래스 (Main Class)
public class CafeMenu {
public static void main(String[] args) {
// 인스턴스 생성 (주문이 들어온 상태)
Americano orderedAmericano = new Americano("Large", 200, 4.5);
// 인스턴스를 통한 객체의 행동 호출 (음료 준비 및 서빙)
orderedAmericano.prepare(); // 음료 준비
orderedAmericano.serve(); // 음료 서빙
// 속성 확인
System.out.println("Ordered Americano Size: " + orderedAmericano.getSize());
System.out.println("Ordered Americano Price: " + orderedAmericano.getPrice());
}
}
객체지향프로그래밍 (OOP, Object-Oriented Programming) 의 특징
1. Class + Object
Class는 객체를 생성하는 설계도 ,
Object는 클래스를 바탕으로 만들어진 실제 인스턴스로,
클래스에서 정의된 속성(Attributes)과 행위(Method)를 실제로 구현한 것이다.
+
Attrbutes : 객체가 가진 데이터
예를 들어 위에 Americano 클래스에서 size, coffeeAmount, price 등이 될 수 있다.
Method : 클래스 내부에 정의된 함수로서 객체가 할 수 있는 행동을 정의한다.
예를 들어 Americano 클래스의 메소드는 prepare(), serve() 등이 있다.
예를 들어
// Americano 클래스를 정의
public class Americano {
// 속성 (Attributes)
private String size;
private int coffeeAmount; // 커피 양 (밀리리터 단위)
private double price;
// 생성자 (Constructor)
public Americano(String size, int coffeeAmount, double price) {
this.size = size;
this.coffeeAmount = coffeeAmount;
this.price = price;
}
// Getter 메소드 (속성 접근)
public String getSize() {
return size;
}
public int getCoffeeAmount() {
return coffeeAmount;
}
public double getPrice() {
return price;
}
// Setter 메소드 (속성 설정)
public void setSize(String size) {
this.size = size;
}
public void setCoffeeAmount(int coffeeAmount) {
this.coffeeAmount = coffeeAmount;
}
public void setPrice(double price) {
this.price = price;
}
// 메소드 (Methods)
public void prepare() {
System.out.println("Preparing the Americano...");
}
public void serve() {
System.out.println("Serving the Americano.");
}
}
// 메인 클래스에서 Americano 클래스를 사용하는 예시
public class Main {
public static void main(String[] args) {
// Americano 객체를 생성합니다.
Americano americano = new Americano("Large", 250, 4.99);
// 속성에 접근합니다.
System.out.println("Size: " + americano.getSize()); //Large 출력
System.out.println("Coffee Amount: " + americano.getCoffeeAmount() + "ml"); // 250ml 출력
System.out.println("Price: $" + americano.getPrice()); // 4.99 출력
// 메소드 호출
americano.prepare(); // Preparing the Americano... 출력
americano.serve(); // Serving the Americano. 출력
// 속성 수정
americano.setSize("Medium"); // 클래스 내부 데이터 수정
americano.setCoffeeAmount(200);
americano.setPrice(3.99);
// 수정된 속성 출력
System.out.println("Updated Size: " + americano.getSize()); // Medium 출력
System.out.println("Updated Coffee Amount: " + americano.getCoffeeAmount() + "ml"); // 200ml 출력
System.out.println("Updated Price: $" + americano.getPrice()); // 3.99 출력
}
}
Americano 클래스는 아메리카노 객체가 가져야 할 속성(Attributes)과 메소드를 정의하고,
메인 클래스는 이 Americano 클래스의 객체를 생성하여(New) 클래스 내부 데이터에 접근하고 메소드를 호출해준다.
2. Inheritance
상속은 한 클래스(자식 클래스)가 다른 클래스(부모 클래스)의 속성과 메소드를 물려받는 것을 의미한다.
이를 통해 자식 클래스는 부모 클래스의 기능을 재사용하면서 추가 기능을 확장하거나 변경할 수 있다.
예를 들면
// 부모 클래스 (Parent Class)
public class Beverage {
// 속성
private String name;
// 생성자
public Beverage(String name) {
this.name = name;
}
// Getter 메소드
public String getName() {
return name;
}
// 메소드 (Method)
public void prepare() {
System.out.println("Preparing the beverage...");
}
// * 오버로딩된 메소드 (Method Overloading) -> 동일한 이름의 메소드를 여러 개 정의하되, 다양한 매개변수 타입과 개수로 호출할 수 있다.
public void serve(int temperature) {
System.out.println("Serving " + name + " at " + temperature + " degrees.");
}
public void serve(String additionalInfo) {
System.out.println("Serving " + name + " with info: " + additionalInfo);
}
}
오버로딩(Overloading)의 정의 :
하나의 클래스 내에서 동일한 이름의 메소드를 여러 개 정의하되, 매개변수 리스트(타입, 개수, 순서)를 다르게
설정한다. 오버로딩된 메소드는 호출될 때 인자의 타입이나 개수에 따라 적절한 메소드가 자동으로 선택된다.
사용하는 이유
- 유연성 : 동일한 메소드 이름을 사용하면서도 매개변수에 따라 다양한 기능을 수행할 수 있다. 예를 들어 prepare() 메소드를 다양한 매개변수로 호출함으로써, 호출 시에 필요한 매개변수에 맞는 동작을 수행할 수 있다.
// 부모 클래스 (Parent Class)
public class Beverage {
// 속성
private String name;
// 생성자
public Beverage(String name) {
this.name = name;
}
// Getter 메소드
public String getName() {
return name;
}
// 오버로딩된 메소드 (Method Overloading)
public void prepare() {
System.out.println("Preparing the beverage...");
}
// 다른 매개변수 리스트를 가진 오버로딩된 메소드
public void prepare(String additionalInfo) {
System.out.println("Preparing the beverage with additional info: " + additionalInfo);
}
// 매개변수 타입이 다른 또 다른 오버로딩된 메소드
public void prepare(int quantity) {
System.out.println("Preparing " + quantity + " units of the beverage.");
}
}
public class Main {
public static void main(String[] args) {
// Beverage 객체 생성
Beverage beverage = new Beverage("Generic Beverage");
beverage.prepare(); // 기본 prepare() 메소드 호출
beverage.prepare("Extra sugar"); // 오버로딩된 prepare(String additionalInfo) 호출
beverage.prepare(5); // 오버로딩된 prepare(int quantity) 호출
}
}
- 코드 가독성 : 메소드 이름을 일관되게 유지할 수 있어 코드의 가족성이 향상된다. 같은 동작을 수행하는 메소드들이 같은 이름을 가지므로, 개발자는 메소드 이름을 보고 어떤 기능을 수행하는지 쉽게 이해할 수 있다.
- 중복 제거 : 메소드 이름을 다르게 하여 중복된 메소드를 정의하는 것보다, 오버로딩을 통해 하나의 메소드 이름으로 다양한 버전을 제공하는 것이 코드 유지 관리에 유리하다.
public class Calculator {
// 덧셈: 두 정수를 더함
public int add(int a, int b) {
return a + b;
}
// 덧셈: 세 정수를 더함
public int add(int a, int b, int c) {
return a + b + c;
}
// 덧셈: 두 실수를 더함
public double add(double a, double b) {
return a + b;
}
// 뺄셈: 두 정수를 뺌
public int subtract(int a, int b) {
return a - b;
}
// 뺄셈: 두 실수를 뺌
public double subtract(double a, double b) {
return a - b;
}
}
// 메인 클래스에서 오버로딩된 메소드 사용 예시
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// 두 정수의 덧셈
int sum1 = calculator.add(5, 10);
System.out.println("Sum of 5 and 10: " + sum1);
// 세 정수의 덧셈
int sum2 = calculator.add(1, 2, 3);
System.out.println("Sum of 1, 2, and 3: " + sum2);
// 두 실수의 덧셈
double sum3 = calculator.add(5.5, 4.5);
System.out.println("Sum of 5.5 and 4.5: " + sum3);
// 두 정수의 뺄셈
int difference1 = calculator.subtract(10, 4);
System.out.println("Difference between 10 and 4: " + difference1);
// 두 실수의 뺄셈
double difference2 = calculator.subtract(10.5, 4.2);
System.out.println("Difference between 10.5 and 4.2: " + difference2);
}
}
// 자식 클래스 (Child Class) - 상속을 받음
public class Coffee extends Beverage {
// 추가 속성
private int coffeeAmount;
// 생성자
public Coffee(String name, int coffeeAmount) {
super(name); // 부모 클래스의 생성자 호출
this.coffeeAmount = coffeeAmount;
}
// 오버라이딩된 메소드 (Overriding)
@Override
public void prepare() {
System.out.println("Preparing the coffee with " + coffeeAmount + " ml of coffee.");
}
// Getter 메소드
public int getCoffeeAmount() {
return coffeeAmount;
}
// 오버로딩된 메소드 사용
public void serve(String additionalInfo, int temperature) {
System.out.println("Serving " + getName() + " with info: " + additionalInfo + " at " + temperature + " degrees.");
}
}
상속의 개념 :
Coffee 클래스는 Beverage 클래스를 상속받았다.
Beverage 클래스의 속성(=필드)과 메소드를 자동으로 사용할 수 있다.
Coffee 클래스는 Beverage 클래스의 모든 public과 protected 메소드를 상속받으며,
이를 통해 Beverage 클래스에서 정의된 기능을 그대로 사용할 수 있습니다.
오버라이딩(Overriding)의 개념 :
상속받은 메소드의 내용(구현)을 수정할 수 있다.
오버라이딩은 부모 클래스에서 정의된 메소드를 자식 클래스에서 재정의하여 사용할 수 있게 한다.
// 메인 클래스에서 상속과 오버라이딩 및 오버로딩을 사용하는 예시
public class Main {
public static void main(String[] args) {
// Beverage 객체 생성
Beverage beverage = new Beverage("Generic Beverage");
beverage.prepare();
beverage.serve(70);
beverage.serve("No ice");
// Coffee 객체 생성
Coffee coffee = new Coffee("Latte", 250);
coffee.prepare(); // 오버라이딩된 메소드 호출
coffee.serve("Extra foam", 60); // 오버로딩된 메소드 호출
coffee.serve(65); // 오버로딩된 메소드 호출
}
}
특징 :
코드 재사용 , 계층 구조, 유연성
+
오버라이딩(Overriding) : 자식 클래스가 부모 클래스에서 상속받은 메소드를 재정의하는 것.
자식 클래스에서 동일한 이름과 파라미터를 가지는 메소드를 새롭게 정의함으로써, 부모 클래스의 메소드 동작을 변경할 수 있다.
메소드 이름, 파마미터, 리턴 타입이 모두 동일해야 한다. 런타임에 어떤 메소드가 호출될지 결정됨(동적 바인딩)
오버로딩(Overloading) : 동일한 이름의 메소드를 여러 개 정의하되, 각 메소드가 서로 다른 파라미터 목록을 가지는 것을 말한다. 즉, 같은 이름을 가진 메소드가 여러 개 존재할 수 있으며, 호출 시 전달된 인자의 타입과 수에 따라 적절한 메소드가 선택된다.
파라미터 목록(타입, 개수)이 달라야 하며, 컴파일 타임에 어떤 메소드가 호출될지 결정됨(컴파일 타임 바인딩)
3. Polymorphism
다형성은 객체가 다양한 형태를 가질 수 있는 능력을 의미한다.
객체가 여러 클래스의 인스턴스로 존재할 수 있고, 동일한 메소드 호출이 객체의 실제 타입에 따라 다르게 동작할 수 있게 한다.
예를 들어
교실에 있을 때 당신은 학생이면서 노트에 필기를 한다.
이커머스 사이트에 들어갔을 땐 고객이다. 물건을 구매하고 팝업을 읽으며 장바구니 포기 이메일을 받는다.
어떤 사람은 가정에서는 아버지이면서, 직장에서는 직원이다.
이러한 예시는 한 사람이 다양한 상황이나 환경에서 서로 다른 역할을 수행하는 방식이다.
다른 역할에 따라 행동이나 반응이 달라지지만, 여전히 동일한 사람이라는 점에서 다형성을 나타낸다.
예시 코드를 살펴보면
Animal이라는 부모 클래스를 가지고 있고, 이 클래스를 상속받는 Dog와 Cat이라는 자식 클래스가 있다고 가정해보자.
// 부모 클래스 (Parent Class)
public class Animal {
// 부모 클래스의 메소드
public void makeSound() {
System.out.println("Some generic animal sound");
}
}
// 자식 클래스 (Child Class) - Dog
public class Dog extends Animal {
// 메소드 오버라이딩
@Override
public void makeSound() {
System.out.println("Bark");
}
}
여기서 Dog와 Cat은 Animal 클래스의 인스턴스로도 존재할 수 있으며,
각각 makeSound() 메소드의 구현이 다르다.
// 자식 클래스 (Child Class) - Cat
public class Cat extends Animal {
// 메소드 오버라이딩
@Override
public void makeSound() {
System.out.println("Meow");
}
}
// 메인 클래스에서 다형성을 사용하는 예시
public class Main {
public static void main(String[] args) {
// Animal 타입의 변수로 Dog 객체를 참조
Animal myAnimal = new Dog(); // 다형성: Animal 타입의 변수에 Dog 객체를 할당
myAnimal.makeSound(); // Dog 클래스의 makeSound() 호출
// Animal 타입의 변수로 Cat 객체를 참조
myAnimal = new Cat(); // 다형성: Animal 타입의 변수에 Cat 객체를 할당
myAnimal.makeSound(); // Cat 클래스의 makeSound() 호출
}
}
다형성의 개념 :
1. 하나의 메소드가 다양한 동작을 수행 :
동일한 메소드 호출이 객체의 실제 타입에 따라 다르게 동작할 수 있다.
예를 들어, Animal 타입의 변수가 Dog 객체를 참조할 때와 Cat 객체를 참조할 때, makeSound() 메소드는 각각 "Bark"과 "Meow"를 출력한다.
2. 객체가 여러 형태를 가질 수 있음 :
Animal 타입의 변수로 Dog과 Cat 객체를 모두 참조할 수 있습니다.
이 변수는 부모 클래스 타입으로 자식 클래스의 객체를 다룰 수 있는 유연성을 제공합니다.
4. Abstraction
추상화는 시스템의 복잡한 세부 구현을 숨기고, 사용자에게 필요한 주요 기능이나 인터페이스만을 제공하는 과정이다.
이를 통해 개발자는 시스템의 복잡성에서 벗어나, 필요한 기능만을 사용하거나 이해할 수 있다.
예를 들어
충상 클래나나 인터페이스를 사용하여 추상화를 구현합니다.
추상 클래스나 인터페이스는 구체적인 구현을 제공하지 않고, 메소드의 시그니처만 정의한다.
구체적인 표현은 서브클래스에서 제공한다.
// 추상 클래스
abstract class Animal {
// 추상 메소드 (구현 없음)
public abstract void makeSound();
}
이 예시에서 Animal 클래스는 추상 메소드 makeSound()를 정의하지만,
실제로 어떻게 소리가 나는지는 Dog와 Cat 클래스에서 구현됩니다.
// 자식 클래스
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat meows");
}
}
사용자는 Animal 타입의 객체를 통해 다양한 동작을 수행할 수 있지만,
구체적인 구현 세부 사항은 숨겨져 있습니다.
// 메인 클래스
public class Main {
public static void main(String[] args) {
Animal myAnimal;
myAnimal = new Dog();
myAnimal.makeSound(); // 출력: Dog barks
myAnimal = new Cat();
myAnimal.makeSound(); // 출력: Cat meows
}
}
특징
복잡성 감소 : 사용자는 시스템의 복잡성을 이해할 필요 없이,
제공된 기능만을 사용하면 된다.
이는 코드의 이해도를 높이고, 유지보수성을 향상시킨다.
인터페이스 제공 : 사용자에게 필요한 작업을 수행하는 인터페이스를 제공하며,
내부 구현의 세부 사항은 숨긴다.
이로 인해 사용자나 다른 객체는 내부 동작을 몰라도 시스템을 효과적으로 사용할 수 있다.
재사용성 향상 : 추상화는 공통된 인터페이스를 정의하여 다양한 구현체를 교체하거나 재사용할 수 있게 한다.
이를 통해 코드의 재사용성과 유연성을 높인다.
5. Encapsulation
캡슐화는 객체의 상태(데이터)를 보호하기 위해, 데이터를 직접 접근하지 못하게 하고,
데이터에 접근하고 조작하는 메소드를 통해서만 데이터에 접근할 수 있도록 하는 것을 의미한다.
예를 들어
public class Person {
// 개인 정보 (데이터)
private String name;
private int age;
// 생성자
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 이름을 얻는 메소드 (공식적인 접근)
public String getName() {
return name;
}
// 나이를 얻는 메소드 (공식적인 접근)
public int getAge() {
return age;
}
// 이름을 설정하는 메소드 (공식적인 접근)
public void setName(String name) {
this.name = name;
}
// 나이를 설정하는 메소드 (공식적인 접근)
public void setAge(int age) {
if (age > 0) { // 나이가 0보다 큰 경우에만 설정
this.age = age;
}
}
}
Person 클래스에서는 클래스 내부 데이터(=attributes)에 접근하기 위해서는
getter , setter 메서드를 사용해서 접근해야만 한다.
Getter와 Setter 메서드
Getter, Setter 메서드를 사용하면 클래스 내부 데이터를 불러오거나(=get) , 수정(=set) 할 수 있다.
이렇게 하는 이유는 다음과 같다.
- 데이터 은닉(Data Hiding) : 클래스 내부 속성을 'private'로 선언함으로써 외부 클래스에서 직접 접근할 수 없게 한다. 이는 데이터 무결성을 보호하고, 잘못된 사용이나 변경을 방지한다.
public class Person {
// Private attributes
private String name;
private int age;
- 유효성 검사 : 'setter' 메서드를 사용하면 속성 값을 설정하기 전에 유효성을 검사하거나 변환할 수 있다. 예를 들어, 나이는 음수로 설정될 수 없도록 할 수 있다.
// Setter method for age
public void setAge(int age) {
if (age > 0) { // Ensure age is positive
this.age = age;
}
}
- 코드 유지보수 : 클래스 내부 데이터의 접근 방식을 중앙 집중화할 수 있기 때문에, 클래스 내부를 변경할 때도 'getter' 와 'setter' 메서드만 수정하면 된다. 외부 클래스에서는 여전히 같은 방법으로 속성에 접근할 수 있다.
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
// Accessing attributes via getter methods
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
// Modifying attributes via setter methods
person.setAge(31);
System.out.println("Updated Age: " + person.getAge());
// Attempting to modify attributes directly (not allowed)
// person.name = "Bob"; // Error: name has private access in Person
// person.age = -5; // Error: age has private access in Person
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
// 데이터 접근을 위한 메소드 사용
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
// 데이터 수정
person.setAge(31);
System.out.println("Updated Age: " + person.getAge());
// 직접 데이터에 접근 불가
// person.name = "Bob"; // 오류 발생
// person.age = -5; // 오류 발생
}
}
Person 클래스의 데이터에 직접 접근하려고 하면 오류가 발생한다.
'기술 공부' 카테고리의 다른 글
REST API에 대해 설명 (0) | 2024.08.04 |
---|---|
MVC에 대한 설명 (0) | 2024.07.31 |