본문 바로가기

Java 특강

객체 지향이란 무엇인가요?

728x90

정의

객체지향 프로그래밍은 프로그램을 객체들의 상호작용으로 구성하는 방식입니다. 각 객체는 상태를 가지고 있고, 그 상태를 변경하거나 활용할 수 있는 행동(메소드)을 합니다. 객체지향 프로그래밍은 이러한 객체들을 중심으로 프로그램을 구조화하여, 복잡한 시스템을 더 쉽게 관리하고 유지 보수할 수 있게 해줍니다.

 

객체지향 프로그래밍의 4가지 특징

캡슐화

객체의 상태와 행동을 묶어, 외부에서 객체의 내부 상태를 직접적으로 접근하지 못하도록 합니다.

각 클래스는 자신의 메서드와 데이터에만 집중하고, 외부에는 필요한 메서드만 공개합니다. 외부 객체들은 이 공개된 메서드를 통해서만 클래스와 상호작용할 수 있습니다. 이를 통해 코드의 복잡성을 줄이고, 수정 및 유지보수 시 유연성을 확보할 수 있습니다.

 

상속

하나의 클래스가 다른 클래스의 속성과 메서드를 물려받아 사용할 수 있는 기능입니다.

만약 상속이 없다면 중복된 코드가 발생하여 코드가 장황해집니다. 파생 클래스를 만들 때마다 이를 다시 작성한다면 효율적이지 못하고, 이후 공통 기능에 수정사항이 있을 때 이를 일일히 다 적용해줘야 합니다.

그리고 공통분모를 가진 이 클래스들간에 아무런 연관성이 만들어지지 않습니다. 속성들과 메소드를 공유하는 클래스들이지만, 이러한 공통점이 프로그래밍에 전혀 반영되지 못합니다.

 

클래스는 다른 클래스에게 속성과 메서드를 물려줄 수 있습니다. 상속받은 하위 클래스는 추가로 가질 속성이나 메서드를 작성하면 됩니다. 그리고 특정 메서드를 다른 방식으로 실행할 목적으로 자식 클래스를 만들 수 있습니다. 이를 오버라이딩이라고 합니다.

 

서로 다른 클래스들을 부모 클래스로 카테고리처럼 묶을 수 있습니다.

카테고리가 되는 것만을 목적으로 하는 클래스도 있습니다. 이를 추상 클래스라고 합니다. 추상 클래스로 객체를 만들 수 없고 메서드의 기능은 자식 클래스에서 결정됩니다. 추상 클래스는 자식 클래스들을 한 데 묶는 카테고리로 작용합니다.

카테고리의 또 다른 방법으로 인터페이스가 있습니다.

인터페이스는 서로 안전히 다른 부모에게 속한 클래스들도 하나의 카테고리로 묶을 수 있습니다. 인터페이스 자체로 객체를 만들 수 없습니다. 인터페이스 전용으로 만든 자료구조, 메서드들이 객체에 들어갈 수 있고, 메서드를 사용해야 하는 상황에서 인터페이스를 가지고 있는 어떤 객체든 동원될 수 있습니다. 어느 객체가 동원됐는지에 따라, 어떻게 실행될지 달라집니다.

 

다형성

하나의 메서드나 객체가 여러 형태로 동작할 수 있는 성질입니다.

다형성을 통해 같은 메서드가 서로 다른 객체에서 다르게 동작하도록 할 수 있습니다. 이를 통해 다양한 형태의 객체를 유연하게 사용할 수 있습니다. 

 

추상화

객체의 세부 사항을 감추고, 필요한 부분만을 외부에 공개하는 개념입니다.

추상 클래스는 구체적인 구현을 포함하지 않고, 자식 클래스에서 구현해야 할 메서드의 틀만 제공합니다. 이를 통해 상위 클래스는 공통된 개념을 정의하고, 하위 클래스에서 각자의 방식으로 구체적인 구현을 할수 있게 합니다. 

 

SOLID란 무엇인지?

객체지향 프로그래밍을 보다 효율적이고 유지보수를 용이하게 하기 위한 다섯 가지 지침들을 SOLID라 불리는 원칙들로 불리고 있습니다.

 

Solid Responsibility Principle

각 클래스는 하나의 책임만 갖고 있어야 합니다. 하나의 책임을 위해 여러 메서드가 존재할 수 있습니다. 단 클래스가 이들을 통해 수행하는 대표적인 업무는 하나이어야 한다는 뜻입니다.

 

위반 예시

public class UserService {
    public void saveUser(User user) {
        // Save user to database
        System.out.println("User saved to database: " + user.getName());
    }

    public void sendWelcomeEmail(User user) {
        // Send welcome email to user
        System.out.println("Welcome email sent to: " + user.getEmail());
    }

    public void logUserActivity(User user) {
        // Log user activity
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

 

옳은 예시

public class UserRepository {
    public void saveUser(User user) {
        // Save user to database
        System.out.println("User saved to database: " + user.getName());
    }
}

public class EmailService {
    public void sendWelcomeEmail(User user) {
        // Send welcome email to user
        System.out.println("Welcome email sent to: " + user.getEmail());
    }
}

public class UserActivityLogger {
    public void logUserActivity(User user) {
        // Log user activity
        System.out.println("Logging activity for user: " + user.getName());
    }
}

class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

public class UserService {
    private UserRepository userRepository = new UserRepository();
    private EmailService emailService = new EmailService();
    private UserActivityLogger userActivityLogger = new UserActivityLogger();

    public void registerUser(User user) {
        userRepository.saveUser(user);
        emailService.sendWelcomeEmail(user);
        userActivityLogger.logUserActivity(user);
    }
}

 

UserService 클래스에는 유저를 데이터베이스에 저장하는 메서드, 환영한다는 이메일을 보내는 메서드, 각 사용자의 활동을 로그에 기록하는 메서드가 있습니다. 이러면 클래스를 수정할 이유가 많아집니다. 메서드 하나를 수정하는 일이 다른 메서드에 의도치 않는 영향을 끼칠 수도 있습니다. 여러 책임이 한 곳에 얽혀 있기 때문에 테스트와 리팩토링도 까다로워집니다. 확장성과 유연성에도 제약이 생깁니다. 책임들이 묶여있다보니 필요한 것만 분리해서 가져다 쓸 수 없습니다.

UserService 하나에 몰아넣었던 세 책임을 세 개의 클래스로 분리한 것을 볼 수 있습니다. 클래스의 이름만 보고도 각각이 어떤 책임을 갖고 있는지 짐작할 수 있습니다.

 

Open-closed Prinicipal

각 클래스가 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙입니다.

 

위반 예시

public class ReportGenerator {
    public void generateReport(String type) {
        if (type.equals("PDF")) {
            System.out.println("Generating PDF report...");
        } else if (type.equals("HTML")) {
            System.out.println("Generating HTML report...");
        }
        // If we need to add another format, we have to modify this method.
    }
}

 

 

옳은 예시

public interface Report {
    void generate();
}

public class PDFReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating PDF report...");
    }
}

public class HTMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating HTML report...");
    }
}

public class XMLReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating XML report...");
    }
}

public class Main {
    public static void main(String[] args) {
        Report pdfReport = new PDFReport();
        pdfReport.generate();  // Generating PDF report...

        Report htmlReport = new HTMLReport();
        htmlReport.generate();  // Generating HTML report...

        Report xmlReport = new XMLReport();
        xmlReport.generate();  // Generating XML report...
    }
}

 

기능이 추가될 때마다 메서드를 수정한다면 코드는 보다 장황해지고 복잡해집니다. 그 과정에서 실수로 기존 가정에 영향을 주게 될 수도 있고, 이미 사용되던 곳들에도 부작용이 발생할 수 있습니다.

인터페이스에 메서드를 만들고 적용한 클래스들이 이 메서드를 오버라이드해서 각자의 방식으로 메서드를 수행합니다. 새로운 형식이 필요하다면 해당 인터페이스를 적용한 다른 클래스를 만들면 됩니다. 이로써 기존의 코드를 수정하는 과정에서 발생할 수 있는 문제들로부터 자유로운 설계를 할 수 있습니다.

 

Liskov Substitution Principle

자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다라는 원칙을 가지고 있습니다.

 

위반 예시

// Parent class Bird
public class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

// Child class Penguin that violates LSP
public class Penguin extends Bird {
    @Override
    public void fly() {
        // Penguins cannot fly
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

public class Main {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.fly(); // Bird is flying

        Bird penguin = new Penguin();
        penguin.fly(); // Throws UnsupportedOperationException
    }
}

 

옳은 예시

// Interface for birds that can fly
public interface Flyable {
    void fly();
}

// Base class Bird
public class Bird {
    public void eat() {
        System.out.println("Bird is eating");
    }
}

// Class for a bird that can fly
public class Sparrow extends Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

// Class for a bird that cannot fly
public class Penguin extends Bird {
    // Penguins do not implement Flyable
}

public class Main {
    public static void main(String[] args) {
        Bird sparrow = new Sparrow();
        sparrow.eat(); // Bird is eating
        ((Flyable) sparrow).fly(); // Sparrow is flying

        Bird penguin = new Penguin();
        penguin.eat(); // Bird is eating
        // ((Flyable) penguin).fly(); // Compilation error, Penguin is not Flyable
    }
}

 

부모 클래스에서 실행되는 메서드를 자식 클래스에서 예외가 발생한다면 틀린 설계가 되는 것입니다. 그래서 해당 메서드를 인터페이스로 정의하여 필요한 클래스에만 적용시킬 수 있습니다. 

 

위반 예시

// Base class
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;
    }
}

// Subclass violating LSP
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height;
    }
}

// Client code
class AreaCalculator {
    public void calculateArea(Rectangle rectangle) {
        rectangle.setWidth(5);
        rectangle.setHeight(4);
        System.out.println("Area: " + rectangle.getArea());
        // Expected output for Rectangle: Area: 20
        // Actual output for Square: Area: 16 (LSP violation)
    }
}

 

옳은 예시

// Base interface
interface Shape {
    int getArea();
}

// Implementations
class Rectangle implements Shape {
    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

class Square implements Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

// Client code
class AreaCalculator {
    public void calculateArea(Shape shape) {
        System.out.println("Area: " + shape.getArea());
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        AreaCalculator calculator = new AreaCalculator();

        // LSP Violation
        Rectangle rectangle = new Rectangle();
        calculator.calculateArea(rectangle); // Output: Area: 20

        Square square = new Square();
        calculator.calculateArea(square); // Output: Area: 16 (Unexpected)

        // LSP Compliant
        Shape rectangle = new Rectangle(5, 4);
        calculator.calculateArea(rectangle); // Output: Area: 20

        Shape square = new Square(5);
        calculator.calculateArea(square); // Output: Area: 25 (Expected)
    }
}

 

만약 부모 클래스의 메서드를 자식 클래스에서 오버라이딩해서 사용된다면 이도 해당 원칙을 위반하게 됩니다. 공통 인터페이스로 카테고리시켜 별개의 클래스로 만들면 해결 가능합니다.

 

Interface Segregation Principle

클래스는 자신이 사용하지 않을 메서드를 구현하도록 강요받지 말아야 합니다.

 

위반 예시

// Interface for workers
public interface Worker {
    void work();
    void eat();
}

// Class representing a regular worker
public class Employee implements Worker {
    @Override
    public void work() {
        System.out.println("Employee is working");
    }

    @Override
    public void eat() {
        System.out.println("Employee is eating");
    }
}

// Class representing a robot
public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }

    @Override
    public void eat() {
        // Robots do not eat
        throw new UnsupportedOperationException("Robots do not eat");
    }
}

public class Main {
    public static void main(String[] args) {
        Worker employee = new Employee();
        employee.work(); // Employee is working
        employee.eat(); // Employee is eating

        Worker robot = new Robot();
        robot.work(); // Robot is working
        robot.eat(); // Throws UnsupportedOperationException
    }
}

 

옳은 예시

// Interface for work-related actions
public interface Workable {
    void work();
}

// Interface for eating-related actions
public interface Eatable {
    void eat();
}

// Class representing a regular worker
public class Employee implements Workable, Eatable {
    @Override
    public void work() {
        System.out.println("Employee is working");
    }

    @Override
    public void eat() {
        System.out.println("Employee is eating");
    }
}

// Class representing a robot
public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot is working");
    }
    // Robot does not implement Eatable interface
}

public class Main {
    public static void main(String[] args) {
        Workable employee = new Employee();
        employee.work(); // Employee is working
        ((Eatable) employee).eat(); // Employee is eating

        Workable robot = new Robot();
        robot.work(); // Robot is working
        // ((Eatable) robot).eat(); // Compilation error, Robot does not implement Eatable
    }
}

 

Dependency Inversion Principle

고수준 모듈이 저수준 모듈에 의해 의존해서는 안된다라는 원칙입니다.

 

위반 예시

// Low-level class
public class Fan {
    public void spin() {
        System.out.println("Fan is spinning");
    }

    public void stop() {
        System.out.println("Fan is stopping");
    }
}

// High-level class
public class Switch {
    private Fan fan;

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

    public void turnOn() {
        fan.spin();
    }

    public void turnOff() {
        fan.stop();
    }
}

 

 

옳은 예시

// Interface for switchable devices
public interface Switchable {
    void turnOn();
    void turnOff();
}

// Low-level class implementing the interface
public class Fan implements Switchable {
    @Override
    public void turnOn() {
        System.out.println("Fan is spinning");
    }

    @Override
    public void turnOff() {
        System.out.println("Fan is stopping");
    }
}

// High-level class
public class Switch {
    private Switchable device;

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

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

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

 

구체적인 공작을 직접 구현하면 저수준 모듈, 추상화된 로직을 제공하면 고수준 모듈입니다. 저수준 클래스 코드들이 고수준 클래스에서 그대로 사용되고 있으면 해당 원칙 위반입니다. 만약 저수준 클래스 메서드의 이름이나 매개변수가 변경된다면 고수준 클래스 메서드의 실행부도 이에 따라 수정되어야 합니다. 이또한 공통 인터페이스를 만들어 고수준 클래스와 저수준 클래스에 적용시킬 수 있습니다. 

 

++

객체지향의 정의

절차지향과의 차이점은?

추상화에 대해서 설명해주세요

다형성에 대해서 설명해주세요

객체와 클래스와의 차이점

리스코프 치환의 원칙이란?

의존성 역전 원칙이란?

단일 책임의 원칙이란?

SOLID가 무엇인가요?

4가지 특징

캡슐화란? 약에 대

'Java 특강' 카테고리의 다른 글