서론
객체 지향의 특징 4가지를 정리할 예정입니다.
1. 캡슐화
2. 추상화
3. 상속
4. 다형성
우선 캡슐화 설명에 들어가기에 앞서 객체 지향이란 무엇일까?
객체 지향이란? from 객체 지향의 창조자 : 엘런케이
딱딱하게 받아들일 개념은 "소프트웨어를 개발 할 때 객체(Obejct) 중심으로 생각한 뒤 해당 객체를 어떤 기능(함수), 특성(데이터)들을 갖추고 있는지 등 모델링하고 재사용할 수 있기 위해 모듈화(기능을 세분화하는 것)하는 것" 입니다.
그러나 객체 지향의 창조자인 엘런케이의 말을 들어보면 조금 더 부드럽게 받아들일 수 있습니다.
(참고로 엘런케이 曰 이라고 쓰여있는 부분은 실제 엘런케이가 말한 내용인지 확실한 팩트체크는 없습니다.)
우연히 유투브를 보다 "객체 지향의 본질"이라는 영상을 보게 됐는데
객체 지향 개념을 처음 만든 엘런케이가 왜 이런 개념을 만들었는지 접할 수 있더라고요. 꼭 보세요! (참고 자료 참조)
엘런케이가 어떻게 객체 지향을 생각하게 됐는지 궁금하다면? 더보기 --
기존의 절차 지향 언어들로 소프트 웨어를 개발하고 정리할 때 프로시저 방법을 사용하고 있었는데
엘런케이는 프로시저로 개발하는 방법이 문제가 있다고 생각했다. 생물학을 복수전공하고 있었던 엘런케이는 세포와 생명체를 보면서 문제를 해결하기 위해서 객체 지향 아이디어를 생각해냈다.
엘런케이 曰 > 프로시저 추상화는 데이터가 여러 프로시저에서 접근이 가능하도록 열려있어. 디버깅하기도 어렵고 조작에 문제가 발생해. 프로그램이 조금만 커지고 복잡해지면 이게 엄청 큰 문제가 될거야. 이걸 어떻게 관리하면 좋을까?
엘런케이 曰 > 시스템을 이루는 세포 하나, 단순하고 독립적이야. 세포들 각각 맡은 역할이 있고 서로 필요한 물질들을 주고받으면서 연결되어있다. 이런식으로 할수 없을까?
아! 소프트웨어를 생명 시스템처럼 구성시키는게 어떨까?
아! 캡슐화를 통해 공유 데이터를 없애서 프로그램을 이해하기도 쉽고 변경하기도 쉽게 만들자.
생물학을 좋아하던 사람으로서 해당 이야기로 객체 지향을 조금 더 쉽게 이해할 수 있었다고 합니다..ㅎㅎ
재밋기도 하고요!
엘런케이가 정말 그시대 융합적 인재가 아니었나.. 싶습니다.
또한 2003년에 앨런 케이가 생각한 객체 지향의 본질은 크게 3가지라고 합니다.
1. 메시징
2. 상태 데이터의 캡슐화
3. 동적 바인딩
우리가 흔히 알고있는 객체 지향의 특징 4가지(캡슐화, 상속, 다형성, 추상화)와 달라보인다?!?
이건 우리가 "How are you? -> I'm fine Thank you, And you?"와 같은 패턴처럼 항상 주입식 학습에만 익숙하기 때문이라고 생각해요. 그러나 위의 3가지는 객체 지향의 본질이고 우리가 알고 있는 건 특징이기 때문에 너무 놀랄 필요가 없습니다.
본질이 있기에 해당 특징들이 존재하는 것이기 때문에 본질을 확실하게 이해하면 객체 지향에 관련된 모든 개념(SOLID, 의존성 주입, 전략 패턴 등등)이 쉽게 와닿을 거에요.
그러나 이번 글에서는 본질을 설명하기보다
특징 4가지 중 캡슐화에 대해서 본질적으로 접근한 관점으로 설명하려고 합니다.
캡슐화란?
일반적인 설명 >
데이터, 그리고 데이터를 활용하는 함수를 캡슐 or 컨테이너 안에 두는 것을 의미.
엘런케이 曰 >
캡슐화를 통해 공유 데이터를 없애서 프로그램을 이해하기도 쉽고 변경하기도 쉽게 만들자!
생명체에 있는 세포처럼 각각의 역할이 다른 작은 세포들로 나누고 의사소통이 필요한 세포들끼리 필요한 걸 주고받을 수 있도록 만들자.
그럼 각각의 세포들로 캡슐화를 하고 세포안에 있는 데이터들을 공유할 수 없도록 제어하고 의사소통이 필요할 경우에는 데이터를 확인하고 바꿀 수 있도록 어떤 기능을 한 세포에 넣어놔야겠다.
自問: 어떻게 캡슐화/ 컨테이너 안에 둔다는거야? / 세포를 캡슐화 한다는 개념이 뭐야?
自答: 하나의 class를 만들어 거기에 필요한 데이터와 함수를 담는다는 거야!
예시 -
캡슐화 전 코드
public class Main {
// 전역 변수로 데이터 선언
static String accountNumber;
static double balance;
public static void main(String[] args) {
// 데이터 설정
accountNumber = "123-456-789";
balance = 1000.0;
// 데이터 출력
printAccountDetails();
// 입금 및 출금
deposit(500.0);
withdraw(200.0);
// 변경된 데이터 출력
printAccountDetails();
}
// 데이터 출력 메소드
public static void printAccountDetails() {
System.out.println("Account Number: " + accountNumber);
System.out.println("Balance: " + balance);
}
// 입금 메소드
public static void deposit(double amount) {
balance += amount;
}
// 출금 메소드
public static void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
} else {
System.out.println("Insufficient balance.");
}
}
}
단점 >
1. 데이터가 전역 변수로 선언되어 있어서 프로그램의 어느 곳이나 접근과 수정이 가능하죠. 이건 엘런케이가 언급했던 문제고 우리가 흔히 알고있는 데이터 무결성에 위배됩니다.
2. 데이터와 그것과 개념적으로 연결되어있는 함수가 분리되어 있어서 코드의 구조가 명확하지 않아요. 우리가 여러번 확인해야 '아, 이것들이 연결되어있구나' 라고 인지할 수 있습니다.
만일 다른 곳에서 입금 값을 달리 하면? - 우리는 원하는 결과를 얻을 수 없겠죠?
관리하기도 어렵고 시스템이 조금만 더 복잡해지면 이해하기도 어려울 게 뻔합니다.
캡슐화가 되어있지 않기 때문에 나눌 수 없기 때문에 코드가 스파게티 코드처럼 복잡해질 거에요.
그럼 캡슐화 한 코드는 어떨까요?
캡슐화 한 코드
class BankAccount {
// private 접근 지정자로 데이터 은닉
private String accountNumber;
private double balance;
// 생성자
public BankAccount(String accountNumber, double balance) {
this.accountNumber = accountNumber;
this.balance = balance;
}
// Getter 메소드
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
// 입금 메소드
public void deposit(double amount) {
balance += amount;
}
// 출금 메소드
public void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
} else {
System.out.println("Insufficient balance.");
}
}
// 데이터 출력 메소드
public void printAccountDetails() {
System.out.println("Account Number: " + accountNumber);
System.out.println("Balance: " + balance);
}
}
public class Main {
public static void main(String[] args) {
// BankAccount 객체 생성 및 데이터 설정
BankAccount account = new BankAccount("123-456-789", 1000.0);
// 데이터 출력
account.printAccountDetails();
// 입금 및 출금
account.deposit(500.0);
account.withdraw(200.0);
// 변경된 데이터 출력
account.printAccountDetails();
}
}
장점>
1. 독립적인 객체: 계좌와 관련되어있는 데이터와 함수(메서드)를 하나의 class 인 BankAccount에 함께 담았습니다.
2. 정보 은닉: 세포(class)에 있는 데이터(accountNumber, balance)를 다른 세포들이 쉽게 접근, 수정할 수 없도록 private 접근 제어자로 막아놨습니다.
3. 코드의 가독성과 유지보수성: 데이터와 그것을 처리하는(개념이 연결되어있는) 함수를 하나의 클래스에 넣음으로서 코드의 가독성과 유지보수성을 높였습니다.
4. 코드 재사용성: 독립적인 BankAccout 클래스로 분리함으로서 계좌에 필요한 서비스로직에 해당 클래스를 재사용 할 수 있습니다.
5. 변경 용이성: 여러 객체(다른 class)들에 연관되어있어도 BankAccount 클래스에서 메서드를 수정하고 추가할 경우 연관되어있는 class에는 영향을 주지 않습니다. 이것은 변경에 용이한 장점이 있죠.
만일 메서드 중에서도 외부에 보여지고 싶지 않으면 public 접근 제어자 대신 private으로 둘 수도 있습니다.
무엇을 노출시키고 무엇을 숨길지 선택할 수 있는거죠.
이렇게 객체 캡슐화를 해놨기 때문에 엘런케이가 말한 본질 중 메시징을 왜 이해하고 알아야하는지 필요성을 알게 됩니다.
왜?
하나의 객체의 데이터가 어디서나 접근이 가능하지 않고 정보가 은닉되어있기 때문에
다른 객체와 데이터를 주고 받으려면 통신할 수 있는 것들이 명확하게 정의된 인터페이스를 설계하고 사용해야합니다.
인터페이스 > 수신자, 요청할 작업(필요한 인자), 결과값 으로 이루어져 있는 것
사실 쉽게 말하면 한 클래스에 public 접근 제어자로 구성되어있는 메서드가 인터페이스라고 이해하면됩니다.
그냥 그 메서드들로 메시지를 주고받는거죠.
이것은 메시징 패싱 방법이라고도 하는데 다음과 같은 코드 예시로 이해할 수 있습니다.
class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: " + amount);
} else {
System.out.println("Deposit amount must be positive.");
}
}
public void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
System.out.println("Withdrew: " + amount);
} else {
System.out.println("Insufficient balance or invalid amount.");
}
}
public void printAccountDetails() {
System.out.println("Account Number: " + accountNumber);
System.out.println("Balance: " + balance);
}
}
class Bank {
private String name;
public Bank(String name) {
this.name = name;
}
public void performTransaction(BankAccount account, String transactionType, double amount) {
System.out.println("Performing " + transactionType + " transaction in " + name + " bank.");
if (transactionType.equals("deposit")) {
account.deposit(amount); // 메시지 패싱
} else if (transactionType.equals("withdraw")) {
account.withdraw(amount); // 메시지 패싱
} else {
System.out.println("Invalid transaction type.");
}
}
public void showAccountDetails(BankAccount account) {
account.printAccountDetails(); // 메시지 패싱
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("123-456-789", 1000.0);
Bank bank = new Bank("Example Bank");
bank.showAccountDetails(account);
bank.performTransaction(account, "deposit", 500.0);
bank.performTransaction(account, "withdraw", 200.0);
bank.showAccountDetails(account);
}
}
Bank, 은행이라는 객체가 있다고 합시다.
예를 들어 하나 은행, 국민 은행 등 여러 은행 이름들이 존재하니 그 이름에 따라 다른 객체를 생성할 수 있겠죠?
이 은행이라는 객체는 BankAccount, 은행 계좌와 연관되어있습니다.
은행은 은행 계좌의 계좌번호와 예금액이라는 데이터에 접근할 수 있어야 합니다.
그래서 은행 계좌 객체의 public 메서드로 메시지를 주고받는 거죠.
정리
캡슐화는 객체 지향 프로그래밍에서 중요한 개념 중 하나로,
클래스 내부에 멤버 변수와 그 변수를 조작하는 메서드를 함께 정의하는 것입니다.
이를 통해 데이터(멤버 변수)를 보호하고, 외부에서의 잘못된 접근을 방지하며, 객체 간의 상호 작용을 보다 안정적으로 관리할 수 있습니다. 외부에서는 직접적으로 멤버 변수에 접근하지 않고 메서드를 통해 간접적으로 데이터에 접근하도록 하는 것이 캡슐화의 한 예입니다.
이를 통해 객체의 내부 구현을 숨기고 외부에는 필요한 인터페이스만 노출시킴으로써 시스템의 유지보수성과 확장성을 향상시킬 수 있습니다.
참고 자료
https://www.youtube.com/watch?v=zgeCwYWzK-k&t=253s
https://www.youtube.com/watch?v=IeLWSKq0xIQ&t=151s
'JAVA > 기초개념' 카테고리의 다른 글
객체 지향의 설계 원칙 SOLID 법칙 (0) | 2024.05.29 |
---|---|
[Java] 객체 지향 언어의 특징 - 다형성 (오버로딩, 오버라이딩) (0) | 2024.05.28 |
[Java] 객체 지향 언어의 특징 - 상속 (0) | 2024.05.28 |
[Java] 객체 지향 언어의 특징 - 추상화 (1) | 2024.05.17 |