객체지향 프로그래밍 OOP
코드를 객체 단위로 나누어 코딩하는 방식으로 다음과 같은 장점이 있다.
코드 재사용성이 높다.
- 새로운 코드를 작성할 때 기존의 코드를 이용해서 쉽게 작성할 수 있다.
코드 관리가 용이하다.
- 코드 간의 관계를 이용해서 적은 노력으로 쉽게 코드를 변경할 수 있다.
신뢰성이 높은 프로그래밍이 가능하다.
- 제어자와 메서드를 이용해서 데이터를 보호하고 올바른 값을 유지하도록 한다.
- 코드의 중복을 제거하여 코드의 불일치로 인한 오작동을 방지할 수 있다.
OOP의 특징
캡슐화
- 필드 변수와 메서드를 클래스로 감싸는 것
- 캡슐화로 인해 모듈화가 가능하여 유지보수성을 높힐 수 있다.
정보은닉
- 캡슐화로 인해 감싸진 데이터들에 접근 제어자 public, private 등을 활용해서 외부에서 접근하지 못하게 할 수 있다.
- 외부 접근을 막아 보안성을 항샹 시키고, 외부에서 필요없는 데이터를 볼 수 없게 하므로써 안정성에도 도움을 준다.
추상화
- 말 그대로 추상적으로 만드는 것이다.
- 추상 메서드는 내용을 구현하지않고 어떤 메서드가 필요한지 정의만 한다. ex) public abstract void makeSound();
- "강아지가 멍멍 짖는다"라는 말은 구체적이다, 하지만 "짖는다"라는 말은 추상적이다. 이와 같이 행위에 대해서만 묘사하고 상속 받는 클래스에서 자신에게 맞게 Override해서 구현을 하면 설계 시 이해가 쉽고, 다양한 구체적인 클래스를 만들 수 있다는 장점이 있다.
상속
- 클래스 간의 상속 관계를 통해 특성과 동작을 이어받아 새로운 클래스를 생성할 수 있다.
- 코드의 재사용성을 높여 중복을 막아준다.
- 클래스 간의 계층 구조를 형성하여 관계를 표현할 수 있다. (= 코드의 이해를 조금 더 쉽게 만들 수 있다)
다형성
- 다형성이란 말 그대로 다른 형태로도 기능을 수행할 수 있게 해주는 것이다.
- 상속과 Override, Overloading 통해 기능을 확장하거나 변경할 수 있다.
- Upcasting, Downcasting, 자식 클래스가 부모 클래스의 기능을 사용하는 것도 다형성의 예이다.
좋은 객체지향 코드를 짜기위한 5가지 원칙 SOLID
처음 객체지향의 원칙을 찾아볼 때 간단한 프로젝트를 기준으로 생각하니 이해가 잘 되지 않았다.
이해를 위해서 코드가 100만 줄이고 자바 파일이 1000개라고 가정하고 읽는 것을 추천한다.
개발을 진행할 때 모든 기능을 혼자서 만들지 않는다. 즉, 하나의 기능을 맡아서 구현하는데 해당 기능을 구현하는데 프로젝트의 모든 코드를 이해할 필요는 없다.
객체의 모든 내용을 구체적으로 파악하는 것은 불가능할 것이고, 내가 필요한 기능과 관련된 파일만 보면 되는 구조가 가장 파악하기 쉬울 것이다.
그리고 코드를 변경했을 때 추가적으로 변경되어야하는 파일이 적을 수록 좋을 것이다.
또한 파일 내부를 보지않더라도 객체의 이름만 보고 또는 구현되지않은 추상 메서드만 보고도 이해할 수 있어야 나의 작업 시간이 단축될 것이다.
이러한 내용을 미리 가정하고 읽는 것을 추천하며, 이를 위해 가장 중요한 핵심은 객체를 역할로써 바라보는 것이다.
또한 이를 코드로 반영하기에 가장 적합한 기술이 인터페이스이다.
단일 책임 원칙(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 클래스에 변경사항이 생기면 기존 코드를 수정해야한다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!