[Java 기본 원리] 객체지향이 뭘까? 함수형 프로그래밍은?
객체 지향 프로그래밍은 구현보다 “설계”에 중점을 둔 프로그래밍 패러다임으로, 4가지 핵심 특징과 5가지 설계 원칙을 기반으로 한다. (캡슐화-접근제어자, 추상화-abstract, interface, 상속, 다형성-오버로딩, 오버라이딩 / SOLID원칙)
객체 지향의 4가지 특징 💡
캡슐화(Encapsulation)
- 데이터와 기능을 하나의 단위로 묶음
- 접근제어자를 통한 정보 은닉
- ex: getter/setter를 통한 제어된 접근
- 참고: java의 default 접근제어자는 class앞에 접근제어자를 생략 시 자동으로 지정!
접근제어자 4개 표
접근제어자 | 같은 클래스 | 같은 패키지 | 다른 패키지의 자식 클래스 | 전체 |
---|---|---|---|---|
private | O | X | X | X |
default(=생략) | O | O | X | X |
protected | O | O | O | X |
public | O | O | O | O |
캡슐화 예시 -> private으로 외부 접근 방지
public class BankAccount {
private int balance; // 외부에서 직접 접근 불가
public void deposit(int amount) {
if (amount > 0) {
this.balance += amount;
}
}
}
// main 메소드 -> BankAccount 클래스 하위가 아니라 가정
BankAccount bank = new BankAccount();
bank.balance = 123; // 수정 시도? 하지만, private으로 불가능
추상화(Abstraction)
- 현실 세계의 공통 속성과 기능을 추출하여 모델링
- 추상 클래스와 인터페이스를 통한 구현 - abstract, interface
- 빠른 설계와 구현이 가능
추상화 예시 -> abstract로 추상 메서드 생성 후 상속받은 자식 클래스(Dog)에서 메서드 구현(makeSound)
public abstract class Animal {
protected String name;
public abstract void makeSound(); // 추상 메서드
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("멍멍!");
}
}
상속(Inheritance)
- 부모 클래스의 특성을 자식 클래스가 물려받아 사용 및 확장
- 코드 재사용성 향상
- 기능의 확장과 변경이 용이
//부모 클래스 Animal
public abstract class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public abstract void makeSound();
public void eat() {
System.out.println(name + "이(가) 먹이를 먹습니다.");
}
}
// 부모 클래스를 상속한 클래스 DOG
public class Dog extends Animal {
private String breed;
// POINT: name, age가 Animal(부모)의 속성!
public Dog(String name, int age, String breed) {
super(name, age);
this.breed = breed;
}
@Override
public void makeSound() {
System.out.println("멍멍!");
}
public void fetch() {
System.out.println(name + "이(가) 공을 가져옵니다.");
}
// POINT: Animal의 eat은 따로 오버라이딩을 하지 않았지만, 부모꺼 바로 사용 가능
}
다형성(Polymorphism)
- 하나의 객체를 여러 타입으로 참조 가능
- 오버라이딩(상속OK) : 자식이 부모의 메소드를 재정의 가능 - 상속받은 메소드 재정의
- 오버로딩(상속NO) : 동일한 함수 이름으로 매개변수만 다르게 구현 가능 - 같은 이름, 다른 매개변수
- 단, 반환타입(함수)은 오버로딩과 관계없음
오버로딩 vs 오버라이딩 비교 표
구분 | 오버로딩 | 오버라이딩 |
---|---|---|
정의 | 같은 이름, 다른 매개변수 | 부모 메서드 재정의 |
메서드 이름 | 동일 | 동일 |
매개변수 | 다름 | 동일 |
리턴 타입 | 상관없음 | 동일 |
접근 제어자 | 상관없음 | 같거나 더 넓은 범위 |
오버라이딩과 오버로딩 예시
// 오버라이딩!!
public class Shape { // 부모 클래스
public double getArea() {
return 0.0;
}
}
public class Circle extends Shape { // 자식 클래스
private double radius;
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape { // 자식 클래스
private double width;
private double height;
@Override
public double getArea() {
return width * height;
}
}
// 오버로딩!!
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
SOLID 원칙 (5가지) 💡
단일 책임 원칙(SRP=Single Responsibility Principle)
- 한 클래스는 하나의 책임만 가져야 함
- 변경 사유가 하나여야 함
SRP 예시 -> 급여 계산, DB 저장, 보고서 생성은 서로 다른 변경 사유를 가짐. 분리함
// 위반 예시 - 하나의 클래스가 여러 책임을 가짐
public class Employee {
public void calculatePay() { /* 급여 계산 */ }
public void saveEmployee() { /* DB 저장 */ }
public void generateReport() { /* 보고서 생성 */ }
}
// 올바른 예시 - 각 클래스가 하나의 책임만 가짐
public class PayCalculator {
public void calculatePay() { /* 급여 계산 로직 */ }
}
public class EmployeeRepository {
public void save() { /* DB 저장 로직 */ }
}
public class ReportGenerator {
public void generateReport() { /* 보고서 생성 로직 */ }
}
개방-폐쇄 원칙(OCP=Open/Closed Principle)
- 확장에는 열려있고 수정에는 닫혀있어야 함
- 인터페이스를 통한 확장 설계
OCP 예시 -> 인터페이스를 통해 다양한 구현체 쉽게 추가. 결제 처리 로직의 변경 없이 새로운 결제 방식 추가함
// 위반 예시 - 새로운 결제 방식 추가시 클래스 수정 필요
public class PaymentProcessor {
public void processPayment(String type) {
if (type.equals("CREDIT")) {
processCreditPayment();
} else if (type.equals("DEBIT")) {
processDebitPayment();
}
// 새로운 결제 방식 추가시 여기를 수정해야 함
}
}
// 올바른 예시 - 인터페이스를 통한 확장
public interface Payment {
void processPayment();
}
public class CreditCardPayment implements Payment {
@Override
public void processPayment() {
System.out.println("신용카드 결제 처리");
}
}
public class DebitCardPayment implements Payment {
@Override
public void processPayment() {
System.out.println("직불카드 결제 처리");
}
}
// 새로운 결제 방식 추가가 쉬움
public class CryptoPayment implements Payment {
@Override
public void processPayment() {
System.out.println("암호화폐 결제 처리");
}
}
리스코프 치환 원칙(LSP=Liskov Substitution Principle)
- 하위 타입은 상위 타입을 대체할 수 있어야 함
- 다형성을 지원하는 객체지향 원칙
LSP 예시 -> 정사각형(Square)이 원(Rectangle)을 상속하면 사각형 설질 위반은 자명! 그래서 인터페이스 도입으로 분리하자
// 위반 예시
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // LSP 위반: 예상치 못한 동작
}
@Override
public void setHeight(int height) {
this.width = height; // LSP 위반: 예상치 못한 동작
this.height = height;
}
}
// 올바른 예시
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private int width;
private int height;
@Override
public int getArea() {
return width * height;
}
}
public class Square implements Shape {
private int side;
@Override
public int getArea() {
return side * side;
}
}
인터페이스 분리 원칙(ISP=Interface Segregation Principle)
- 클라이언트별 인터페이스 분리
- 작은 단위의 구체적인 인터페이스 여러 개가 범용 인터페이스 하나보다 좋음
ISP 예시 -> 불필요한 의존성을 제거하여 유연성을 향상하자. 사람과 로봇의 기능이 다른것은 자명! 따라서 인터페이스를 작은 단위로 분리하는게 이 상황에서 좋다.
// 위반 예시 - 필요 없는 메서드까지 구현해야 함
public interface Worker {
void work();
void eat();
void sleep();
}
// 올바른 예시 - 인터페이스를 역할별로 분리
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
// 사람은 모든 기능이 필요
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() {
System.out.println("일하기");
}
@Override
public void eat() {
System.out.println("식사하기");
}
@Override
public void sleep() {
System.out.println("잠자기");
}
}
// 로봇은 일만 함
public class Robot implements Workable {
@Override
public void work() {
System.out.println("일하기");
}
}
의존관계 역전 원칙(DIP=Dependency Inversion Principle)
- 추상화에 의존하고 구체화에 의존하지 않음
- 구현 클래스보다 인터페이스에 의존
DIP 예시 -> 구체적인 구현보다는 인터페이스에 의존하여 유연성 확보하자. 인터페이스 사용한 것이 메일 발송 유연성과 알림 서비스 확장에 유용하단 걸 알 수 있다.
// 위반 예시 - 구체 클래스에 직접 의존
public class EmailService {
private SmtpClient smtpClient; // 구체 클래스에 의존 (SMTP:인터넷 메시지 통신 프로토콜)
public void sendEmail(String message) {
smtpClient.send(message);
}
}
// 올바른 예시 - 추상화에 의존
public interface MessageService {
void sendMessage(String message);
}
public class EmailService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("이메일 발송: " + message);
}
}
public class SMSService implements MessageService {
@Override
public void sendMessage(String message) {
System.out.println("SMS 발송: " + message);
}
}
public class NotificationService {
private final MessageService messageService; // 인터페이스에 의존
public NotificationService(MessageService messageService) {
this.messageService = messageService;
}
public void notify(String message) {
messageService.sendMessage(message);
}
}
명령형, 함수형 프로그래밍이 뭔데??
절차지향, 객체지향은 명령형 프로그래밍(어떻게)
함수형은 선언형 프로그래밍(무엇을)
함수형 프로그래밍은?? 프로그래밍 패러다임 참고
-
어떻게 할것인지 보다 무엇을 할것인지에 포커스
-
수학 함수 형태
-
메모리 개념 없음
- 변수 및 대입문(배정문. 메모리 할당) 없음
-
반복문 없음
- 대안 : 리커전 반복
-
매개변수도 함수를 넘김
-
상태와 가변 데이터 지양
-
순수 함수 사용
// 순수 함수 public int add(int a, int b) { return a + b; // 동일 입력 = 동일 출력, 부작용 없음 } // 순수하지 않은 함수 private int total = 0; public void add(int value) { total += value; // 외부 상태 변경 }
-
명령형 vs 선언형 프로그래밍
명령형 프로그래밍 방식 - 절차,객체 지향
- 실행 과정을 하나하나 명시
- 리스트 생성, 반복문 제어, 인덱스 관리 등을 직접 처리
- “어떻게(How)” 처리할지 모든 과정을 프로그래머가 작성
javaList<Integer> doubled = new ArrayList<>();
for(int i = 0; i < numbers.size(); i++) {
doubled.add(numbers.get(i) * 2);
}
선언형 프로그래밍 방식 - 함수 지향
- 원하는 결과를 선언
- 데이터의 흐름을 체인 형태로 표현
- “무엇을(What)” 할지만 정의하고 세부 구현은 추상화
javaList<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
동작 흐름을 보고 이해하자!
명령형 방식의 동작 - How에 가까움.
- 빈 ArrayList 객체 생성
- 반복문으로 인덱스 0부터 순회
- 각 인덱스의 값을 2배로 만들어 새 리스트에 추가
- 모든 요소 처리 완료까지 2-3 반복
선언형 방식의 동작 -> What에 가까움. 함수로.
- numbers를 스트림으로 변환
- map 함수로 모든 요소를 2배로 변환
- collect로 결과를 리스트로 수집
장단점은??
명령형 프로그래밍
- 장점: 실행 과정이 명확히 보임
- 단점: 코드가 길어지고 복잡해짐
선언형 프로그래밍
- 장점: 간결하고 의도가 명확히 드러남
- 단점: 내부 동작 이해가 필요함
댓글남기기