객체지향 프로그래밍 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 |