소프트웨어 개발에서 객체 지향 설계는 매우 중요한 원칙입니다.👀
객체 지향 설계는 코드의 재사용성, 확장성, 유지보수성을 높여주며, 복잡한 시스템을 쉽게 이해하고 관리할 수 있게 합니다.
객체지향 설계의 주요 개념
클래스(class)와 객체(object)
클래스는 객체를 생성하기 위한 템플릿입니다. 클래스는 객체의 속성(필드)과 행동(메서드)을 정의합니다.
예를 들어, Car 클래스는 color, model 등의 속성과 drive(), stop() 등의 메서드를 가질 수 있습니다.
public class Car {
private String color;
private String model;
public Car(String color, String model) {
this.color = color;
this.model = model;
}
public void drive() {
System.out.println("The car is driving.");
}
public void stop() {
System.out.println("The car has stopped.");
}
}
객체는 클래스의 인스턴스입니다.
예를 들어, Car 클래스에서 myCar 객체를 생성할 수 있습니다.
Car myCar = new Car("Red", "Tesla");
myCar.drive();
상속(Inheritance)
상속은 한 클래스가 다른 클래스의 속성과 메서드를 상속받아 사용하는 개념입니다.
이를 통해 코드의 재사용성을 높이고, 클래스 간의 관계를 표현할 수 있습니다.
public class ElectricCar extends Car {
private int batteryLife;
public ElectricCar(String color, String model, int batteryLife) {
super(color, model);
this.batteryLife = batteryLife;
}
public void charge() {
System.out.println("The car is charging.");
}
}
- 코드 재사용
부모 클래스에서 정의한 속성과 메서드를 자식 클래스에서 재사용할 수 있어 코드 중복을 줄입니다. - 계층 구조
클래스 간의 관계를 계층 구조로 표현할 수 있어 코드의 구조를 명확히 합니다. - 유지보수 용이성
부모 클래스의 변경 사항이 자식 클래스에 자동으로 반영되므로 유지보수가 용이합니다.
다형성(Polymorphism)
다형성은 같은 메서드가 다른 객체에서 다른 방식으로 동작할 수 있는 능력입니다.
이는 주로 메서드 오버로딩과 오버라이딩을 통해 구현됩니다.
Car myElectricCar = new ElectricCar("Blue", "Tesla", 100);
Car myGasolineCar = new GasolineCar("Red", "Toyota", 50);
- 유연성
코드가 더 유연해지고, 다양한 객체를 동일한 방식으로 처리할 수 있습니다. - 확장성
새로운 클래스를 추가할 때 기존 코드를 수정하지 않고도 확장이 가능합니다. - 유지보수성
코드의 유지보수가 쉬워지며, 중복 코드를 줄일 수 있습니다. - 재사용성
공통된 인터페이스나 부모 클래스를 통해 코드를 재사용할 수 있습니다.
캡슐화(Encapsulation)
객체의 데이터를 외부로부터 보호하고 객체의 내부 구현을 숨기는 것을 말합니다.
이를 통해 객체는 데이터와 이를 조작하는 메서드를 하나의 단위로 묶어, 외부에서는 해당 객체의 데이터에 직접 접근하지 못하고, 제공된 메서드를 통해서만 접근할 수 있습니다.
public class Car {
private String color;
private String model;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
- 정보은닉
객체의 내부 구현을 숨김으로써 외부에서 객체의 상태를 직접 변경할 수 없도록 합니다. - 모듈성 향상
객체의 데이터와 메서드를 하나의 단위로 묶어, 코드의 모듈성을 향상합니다. - 유지보수성 향상
객체의 내부 구현이 변경되더라도 외부 코드에 영향을 미치지 않으므로 유지보수가 용이합니다. - 재사용성 향상
객체의 인터페이스를 통해 외부 상호작용하므로, 객체를 다른 프로그램에서 쉽게 재사용할 수 있습니다.
추상화(Abstraction)
추상화는 복잡한 시스템을 단순화하여 핵심적인 개념만을 모델링하는 것입니다.
인터페이스와 추상 클래스를 통해 구현됩니다.
public abstract class Car {
private String color;
private String model;
public abstract void drive();
public abstract void stop();
}
객체 지향 설계의 원칙
소프트웨어 개발에서 코드를 더 이해하기 쉽고, 유지보수하기 쉽게 만드는 중요한 지침들입니다.
가장 대표적인 원칙으로 SOLID원칙이 있습니다.
이 원칙들은 클래스와 객체 간 관계를 정의하는 방법을 제공하여 코드의 유연성과 재사용성을 높여줍니다.
1. 단일 책임 원칙(Single Responsibility Principle, SRP)
클래스는 하나의 책임만 가져야 합니다. 이는 클래스를 변경해야 하는 이유가 단 하나뿐이어야 함을 의미합니다.
만약 여러 가지 일을 한 클래스에서 다 하면, 수정해야 할 때 어디를 고쳐야 할지 혼란스러워질 수 있습니다.
class Book {
private String title;
private String author;
// 책 내용을 출력하는 메서드
public void printContent() {
// 출력 로직
}
}
여기서 Book 클래스는 책 정보를 나타내고 printContent 메서드는 출력 기능을 가지고 있습니다.
출력 기능은 다른 클래스로 분리하는 것이 좋습니다.
2. 개방-폐쇄 원칙(Open/Closed Principle, OCP)
소프트웨어 요소는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 합니다.
새로운 기능을 추가할 때 기존 코드를 수정하지 말고, 새로운 코드를 추가해야 합니다.
abstract class Shape {
abstract void draw();
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Draw Circle");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Draw Rectangle");
}
}
여기서 Shape 클래스는 추상 클래스로 새로운 형태의 도형을 추가할 때 Shape을 상속받는 새로운 클래스를 추가하면 됩니다.
3. 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 대체할 수 있어야 합니다.
자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 합니다.
class Bird {
void fly() {
System.out.println("Flying");
}
}
class Ostrich extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("Ostriches can't fly");
}
}
이 예시에서 Ostrich는 Bird를 상속받았지만, fly 메서드를 제대로 구현할 수 없습니다.
이는 Ostrich 객체를 Bird 타입으로 사용할 때 문제가 발생할 수 있음을 의미합니다.
Bird bird = new Ostrich();
bird.fly(); // This will throw an exception
LSP를 준수하려면, Bird 클래스의 구조를 변경하여 모든 새가 fly 메서드를 반드시 가질 필요가 없도록 해야 합니다.
이를 위해 인터페이스를 사용할 수 있습니다.
interface Flyable {
void fly();
}
class Bird {
// 새에 대한 공통 기능들
}
class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
class Ostrich extends Bird {
// 타조는 날지 않음, Flyable을 구현하지 않음
}
이 구조에서는 Flyable 인터페이스를 새롭게 정의하고, 날 수 있는 새들만 이 인터페이스를 구현합니다.
이렇게 하면 타조는 Flyable 인터페이스를 구현하지 않기 때문에 fly 메서드를 갖지 않게 되어 LSP를 준수하게 됩니다.
Flyable bird = new Sparrow();
bird.fly(); // This will work
Bird ostrich = new Ostrich();
// ostrich.fly(); // This won't compile, preventing runtime exceptions
이처럼 LSP를 준수하면, 자식 클래스가 부모 클래스를 완벽히 대체할 수 있고, 예상치 못한 예외나 오류를 방지할 수 있습니다.
4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫습니다.
하나의 큰 인터페이스보다는, 여러 개의 작은 인터페이스로 분리하는 것이 좋습니다.
interface Worker {
void work();
void eat();
}
class HumanWorker implements Worker {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
}
class RobotWorker implements Worker {
@Override
public void work() {
System.out.println("Robot working");
}
@Override
public void eat() {
// 로봇은 먹지 않음
throw new UnsupportedOperationException("Robots don't eat");
}
}
여기서 Worker 인터페이스는 work와 eat 메서드를 가지고 있는데 로봇은 먹지 않으므로 eat 메서드를 구현할 필요가 없습니다.
따라서 Worker 인터페이스를 Workable과 Eatable로 분리하는 것이 좋습니다.
5. 의존 역전 원칙(Dependency Inversion Principle, DIP)
고수준 모듈은 저수준 모듈에 의존해서는 안되며 둘 다 추상화에 의존해야 합니다.
구체적인 것보다는 추상적인 것에 의존하도록 코드를 작성해야 합니다.
class Light {
public void turnOn() {
System.out.println("Light is on");
}
}
class Switch {
private Light light;
public Switch(Light light) {
this.light = light;
}
public void operate() {
light.turnOn();
}
}
여기서 Switch 클래스는 Light 클래스에 의존합니다. 이를 인터페이스로 바꾸면 의존성을 줄일 수 있습니다.
interface Switchable {
void turnOn();
}
class Light implements Switchable {
@Override
public void turnOn() {
System.out.println("Light is on");
}
}
class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void operate() {
device.turnOn();
}
}
💡객체 지향 설계는 복잡한 소프트웨어 시스템을 더 쉽게 관리하고 확장할 수 있게 하는 중요한 설계 방법론입니다.
Java를 활용하여 주요 개념과 설계 원칙을 잘 준수함으로써 더 나은 품질의 소프트웨어를 개발할 수 있습니다.
'Java' 카테고리의 다른 글
[Java] final 불변 객체를 사용해야하는 이유 (0) | 2024.08.10 |
---|---|
[Java] 일급 컬렉션(First-Class Collection)이란? (0) | 2024.07.31 |
[Java] ConcurrentHashMap 멀티스레드 환경에서 안전한 해시맵 (0) | 2024.07.28 |
[Java] computeIfAbsent 효율적인 데이터 캐싱과 값 처리 (0) | 2024.07.27 |
[Java] abstract class 와 interface class의 차이 (0) | 2024.07.17 |