[JAVA] OOP

객체지향 프로그래밍 OOP

코드를 객체 단위로 나누어 코딩하는 방식으로 다음과 같은 장점이 있다.

 

코드 재사용성이 높다.

  • 새로운 코드를 작성할 때 기존의 코드를 이용해서 쉽게 작성할 수 있다.

코드 관리가 용이하다.

  • 코드 간의 관계를 이용해서 적은 노력으로 쉽게 코드를 변경할 수 있다.

신뢰성이 높은 프로그래밍이 가능하다.

  • 제어자와 메서드를 이용해서 데이터를 보호하고 올바른 값을 유지하도록 한다.
  • 코드의 중복을 제거하여 코드의 불일치로 인한 오작동을 방지할 수 있다.

OOP의 특징

캡슐화

  • 필드 변수와 메서드를 클래스로 감싸는 것
  • 캡슐화로 인해 모듈화가 가능하여 유지보수성을 높힐 수 있다.

정보은닉

  • 캡슐화로 인해 감싸진 데이터들에 접근 제어자 public, private 등을 활용해서 외부에서 접근하지 못하게 할 수 있다.
  • 외부 접근을 막아 보안성을 항샹 시키고, 외부에서 필요없는 데이터를 볼 수 없게 하므로써 안정성에도 도움을 준다.

추상화

  • 말 그대로 추상적으로 만드는 것이다. 
  • 추상 메서드는 내용을 구현하지않고 어떤 메서드가 필요한지 정의만 한다. ex) public abstract void makeSound();
  • "강아지가 멍멍 짖는다"라는 말은 구체적이다, 하지만 "짖는다"라는 말은 추상적이다. 이와 같이 행위에 대해서만 묘사하고 상속 받는 클래스에서 자신에게 맞게 Override해서 구현을 하면 설계 시 이해가 쉽고, 다양한 구체적인 클래스를 만들 수 있다는 장점이 있다.

상속

  • 클래스 간의 상속 관계를 통해 특성과 동작을 이어받아 새로운 클래스를 생성할 수 있다.
  • 코드의 재사용성을 높여 중복을 막아준다.
  • 클래스 간의 계층 구조를 형성하여 관계를 표현할 수 있다. (= 코드의 이해를 조금 더 쉽게 만들 수 있다)

다형성

  • 다형성이란 말 그대로 다른 형태로도 기능을 수행할 수 있게 해주는 것이다.
  • 상속과 Override, Overloading 통해 기능을 확장하거나 변경할 수 있다.
  • Upcasting, Downcasting, 자식 클래스가 부모 클래스의 기능을 사용하는 것도 다형성의 예이다.

좋은 객체지향 코드를 짜기위한 5가지 원칙 SOLID

 

단일 책임 원칙(Single Responsiblity Principle, SRP)

  • 클래스나 모듈은 단 하나의 책임만을 가진다.
  • 클래스가 여러가지 이유로 변형될 가능성을 줄인다.
  • 한 가지 변경 사항이 다른 기능에 영향을 미치는 상황을 최소화 하여 코드의 유지보수성을 증가시킨다.
//잘못된 코드
class OrderManager {
    public void createOrder(Order order) {
        // 주문 생성 로직
    }
    
    public void processPayment(Order order, Payment payment) {
        // 결제 처리 로직
    }
    
    public void sendConfirmationEmail(Order order) {
        // 이메일 발송 로직
    }
}


//단일 책임 원칙을 지킨 코드
class OrderManager {
    public void createOrder(Order order) {
        // 주문 생성 로직
    }
}

class PaymentProcessor {
    public void processPayment(Order order, Payment payment) {
        // 결제 처리 로직
    }
}

class EmailSender {
    public void sendConfirmationEmail(Order order) {
        // 이메일 발송 로직
    }
}

하나의 클래스에 모든 기능을 넣지않고 기능을 책임지는 클래스를 만든다.

 

개방/폐쇄 원칙(Open/Closed Principle, OCP)

  • 기존의 코드를 수정하지 않으면서 기능을 확장할 수 있어야한다.
  • 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계한다.
  • 인터페이스와 추상화를 통해 이 원칙을 구현할 수 있다.
interface Shape {
    double calculateArea();
}


class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

shape의 코드를 건드리지 않고 Circle을 확장할 수 있다.

 

리스코프 치환 원칙(Liskov Substitution Principle, LSP)

  • 하위 클래스는 부모 클래스의 역할을 완전히 대신할 수 있어야 한다.
  • 상속 관계에서 하위 클래스는 부모 클래스의 기능을 무시하지 않고 재정의하거나 확장하여야 한다.
  • 이를 통해 코드의 안정성과 일관성을 유지한다.
class Animal {
    public void makeSound() {
        System.out.println("Some animal sound");
    }
    
    public void eat(){
    	System.out.println("eat food");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof woof");
    }
    
    @Override
    public void eat(){
    	System.out.println("eat meat");
    }
}

자식 클래스에서 부모 클래스의 모든 기능을 사용할 수 있다.

 

인터페이스 분리 원칙(Interface Segregation Principle, ISP)

  • 하나의 인터페이스에 너무 많은 기능을 넣으면 구현체가 필요없는 기능까지 Override해야한다.
  • 인터페이스에 필요한 기능만 넣어 작게 만든다.
  • 이를 통해 불필요한 종속성을 줄이고 인터페이스의 응집성을 높인다.
//인터페이스 분리원칙을 어긴 예시

interface Printer {
    void print();
    void charge();
}

class InkjetPrinter implements Printer {
    @Override
    public void print() {
        System.out.println("Printing with inkjet printer");
    }
    
    @Override
    public void charge(){
    	System.out.println("Charge inkjet printer");
    }
}

class LaserPrinter implements Printer {
    @Override
    public void print() {
        System.out.println("Printing with laser printer");
    }
    
    @Override
    public void charge(){
    
    }
}

LaserPrinter는 충전 기능이 필요없는데도 구현해야한다.

 

 

의존관계 역전 원칙(Dependency Inversion Principle, DIP)

  • 고수준 모듈은 저수준 모듈에 의존하면 안된다. 둘 모두 추상화에 의존해야 한다.
  • 추상화된 인터페이스나 추상 클래스를 통해 의존 관계를 만든다.
  • 쉽게 말해서 의존성을 떨어트려 느슨한 결합 상태로 만들어야한다.
  • 이를 통해 유연하고 변경 가능한 구조를 갖춘 코드를 작성할 수 있다.
interface Switchable {
    void turnOn();
    void turnOff();
}

class LightBulb implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("LightBulb: Bulb turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("LightBulb: Bulb turned off");
    }
}

class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Fan: Fan turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("Fan: Fan turned off");
    }
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void turnOn() {
        device.turnOn();
    }

    public void turnOff() {
        device.turnOff();
    }
}

public class DIPExample {
    public static void main(String[] args) {
        LightBulb lightBulb = new LightBulb();
        Fan fan = new Fan();

        Switch bulbSwitch = new Switch(lightBulb);
        Switch fanSwitch = new Switch(fan);

        bulbSwitch.turnOn();
        bulbSwitch.turnOff();

        fanSwitch.turnOn();
        fanSwitch.turnOff();
    }
}

이렇게 되면 고수준 모듈인 Switch는 저수준 모듈인 LightBulb와 Fan에 의존하지않고, Switchable 인터페이스에 의존하게된다.

예를 들어 Switch에 공통적으로 들어갈 기능이 생기면 interface에 기능을 추가하고 device에서 구현하면된다. 즉 기존 코드를 수정할 필요 없이 추가된 내용만 작성하면 된다.

이해가 잘 되지않을 수 있기 때문에 아래 나쁜 예시 코드가 있다.

 

class LightBulb {
    void turnOn() {
        System.out.println("LightBulb turned on");
    }

    void turnOff() {
        System.out.println("LightBulb turned off");
    }
}

class Fan {
    void turnOn() {
        System.out.println("Fan turned on");
    }

    void turnOff() {
        System.out.println("Fan turned off");
    }
}

class Switch {
    private LightBulb lightBulb;
    private Fan fan;

    public Switch(LightBulb lightBulb, Fan fan) {
        this.lightBulb = lightBulb;
        this.fan = fan;
    }

    void turnOnLights() {
        lightBulb.turnOn();
    }

    void turnOffLights() {
        lightBulb.turnOff();
    }

    void turnOnFan() {
        fan.turnOn();
    }

    void turnOffFan() {
        fan.turnOff();
    }
}

public class DirectDependencyExample {
    public static void main(String[] args) {
        LightBulb lightBulb = new LightBulb();
        Fan fan = new Fan();

        Switch manualSwitch = new Switch(lightBulb, fan);

        manualSwitch.turnOnLights();
        manualSwitch.turnOnFan();

        manualSwitch.turnOffFan();
        manualSwitch.turnOffLights();
    }
}

Fan이나 LightBlub 클래스에 변경사항이 생기면 기존 코드를 수정해야한다.

'개념 정리' 카테고리의 다른 글

[AWS] IAM  (0) 2023.08.03
[Cloud] CDN  (0) 2023.07.29
[Cloud] IaaS, PaaS, SaaS  (0) 2023.06.28
[Network] 프록시 개념  (0) 2023.06.02