[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에 가까움.

  1. 빈 ArrayList 객체 생성
  2. 반복문으로 인덱스 0부터 순회
  3. 각 인덱스의 값을 2배로 만들어 새 리스트에 추가
  4. 모든 요소 처리 완료까지 2-3 반복


선언형 방식의 동작 -> What에 가까움. 함수로.

  1. numbers를 스트림으로 변환
  2. map 함수로 모든 요소를 2배로 변환
  3. collect로 결과를 리스트로 수집



장단점은??

명령형 프로그래밍

  • 장점: 실행 과정이 명확히 보임
  • 단점: 코드가 길어지고 복잡해짐


선언형 프로그래밍

  • 장점: 간결하고 의도가 명확히 드러남
  • 단점: 내부 동작 이해가 필요함

댓글남기기