좋은 설계란 시스템에 새로운 요구사항이나 변경사항이 있을 때, 영향을 받는 범위가 적은 구조를 말한다. 그래서 시스템에 예상하지 못한 변경사항이 발생하더라도, 유연하게 대처하고 이후에 확장성이 있는 시스템 구조를 만들 수 있다.
즉, SOLID 객체 지향 원칙을 적용하면 코드를 확장하고 유지 보수 관리하기가 더 쉬워지며, 불필요한 복잡성을 제거해 리팩토링에 소요되는 시간을 줄임으로써 프로젝트 개발의 생산성을 높일 수 있다.
SRP 위배 예제
// 주문 처리 클래스 - 주문 처리와 이메일 발송 두 가지 책임을 가지고 있음
class OrderProcessor {
public void processOrder(String orderDetails) {
// 주문 처리 로직...
sendConfirmationEmail(orderDetails); // 이메일 발송
}
// 이메일 발송 메서드
private void sendConfirmationEmail(String orderDetails) {
System.out.println("이메일을 발송했습니다: 주문 내용 - " + orderDetails);
}
}
// 메인 클래스 - 주문 처리 클래스를 사용하여 주문 처리와 이메일 발송
public class Main {
public static void main(String[] args) {
OrderProcessor orderProcessor = new OrderProcessor();
String orderDetails = "상품명: ABC, 가격: $50";
orderProcessor.processOrder(orderDetails); // 주문 처리와 이메일 발송
}
}
OrderProcessor
클래스는 주문 처리와 이메일 발송 두 가지 책임을 가지고 있습니다. 이렇게 되면 주문 처리 로직의 변경이 이메일 발송에도 영향을 미칠 수 있다.SRP 준수 예제
// 주문 처리 클래스 - 주문 처리에 대한 책임만을 가지고 있음
class OrderProcessor {
public void processOrder(String orderDetails) {
// 주문 처리 로직...
System.out.println("주문을 처리했습니다: 주문 내용 - " + orderDetails);
}
}
// 이메일 서비스 클래스 - 이메일 발송에 대한 책임만을 가지고 있음
class EmailService {
public void sendConfirmationEmail(String orderDetails) {
System.out.println("이메일을 발송했습니다: 주문 내용 - " + orderDetails);
}
}
// 메인 클래스 - 주문 처리와 이메일 발송 클래스를 사용하여 주문 처리와 이메일 발송
public class Main {
public static void main(String[] args) {
OrderProcessor orderProcessor = new OrderProcessor();
EmailService emailService = new EmailService();
String orderDetails = "상품명: ABC, 가격: $50";
orderProcessor.processOrder(orderDetails); // 주문 처리
emailService.sendConfirmationEmail(orderDetails); // 이메일 발송
}
}
OrderProcessor
클래스는 주문 처리에만 집중하고, EmailService
클래스는 이메일 발송에만 집중하게 되었다. 이렇게 분리된 코드는 각 클래스의 변경이 다른 클래스에 영향을 미치지 않게 되었다.SRP 원칙의 책임의 범위
- 실제 이 원리를 적용해서 직접 클래스를 설계하기는 쉽지 않다.
- 단일 책임 기준은 살마들마다 생각이 다르고 상황이 달라질 수 있기 때문이다.
OCP 위배 예제
// 도형 클래스 - OCP를 위반한 예제
class Shape {
String type;
public Shape(String type) {
this.type = type;
}
public double calculateArea() {
if (type.equals("Circle")) {
// 원의 면적 계산 로직...
return /* 원의 면적 */;
} else if (type.equals("Rectangle")) {
// 사각형의 면적 계산 로직...
return /* 사각형의 면적 */;
}
// 다른 도형의 면적 계산 로직도 추가...
return 0.0;
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
Shape circle = new Shape("Circle");
double circleArea = circle.calculateArea();
System.out.println("원의 면적: " + circleArea);
Shape rectangle = new Shape("Rectangle");
double rectangleArea = rectangle.calculateArea();
System.out.println("사각형의 면적: " + rectangleArea);
}
}
Shape
클래스는 도형의 종류에 따라 면적을 계산하는 책임을 가지고 있다. 하지만 새로운 도형이 추가 될 때마다 calculateArea
메서드를 수정해야 하므로 OCP 원칙을 위반하고 있다.OCP 준수 예제
// 도형 인터페이스 - OCP를 준수한 예제
interface Shape {
double calculateArea();
}
// 원 클래스 - 도형 인터페이스를 구현
class Circle implements Shape {
@Override
public double calculateArea() {
// 원의 면적 계산 로직...
return /* 원의 면적 */;
}
}
// 사각형 클래스 - 도형 인터페이스를 구현
class Rectangle implements Shape {
@Override
public double calculateArea() {
// 사각형의 면적 계산 로직...
return /* 사각형의 면적 */;
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
Shape circle = new Circle();
double circleArea = circle.calculateArea();
System.out.println("원의 면적: " + circleArea);
Shape rectangle = new Rectangle();
double rectangleArea = rectangle.calculateArea();
System.out.println("사각형의 면적: " + rectangleArea);
}
}
Shape
인터페이스를 도입하였고 각 도형은 위 인터페이스를 구현하여 새로운 도형이 추가될 때마다 기존 코드를 수정할 필요 없이 새로운 도형을 추가하여 확장 할 수 있다.OCP 원칙을 따른 JDBC
- OCP 원칙의 가장 잘 따르는 예시가 바로 자바의 데이터베이스 인터페이스인 JDBC이다.
- 만일 자바 애플리케이션에서 사용하고 있는 데이터베이스를 MySQL에서 Oracle로 바꾸고 싶다면, 복잡한 하드 코딩 없이 그냥 connection 객체 부분만 교체해주면 된다.
LSP 위배 예제
class Bird {
public void fly() {
System.out.println("새가 날아갑니다.");
}
}
class Penguin extends Bird {
@Override
public void fly() {
System.out.println("펭귄은 날지 못해요.");
}
public void swim() {
System.out.println("펭귄이 수영해요.");
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Penguin();
bird.fly(); // 예상 결과: 펭귄은 날지 못해요.
// 컴파일러는 fly 메서드는 있지만 swim 메서드는 없다고 판단하여 에러 발생
// ((Penguin) bird).swim(); // 컴파일 에러
}
}
Penguin
클래스가 Bird
클래스를 상속받았지만,Penguin
에만 있는 swim
메서드를 사용하려고 하면 컴파일러에서는 Bird
타입으로 선언된 변수에서 swim
메서드를 찾을 수 없다고 판단하여 에러가 발생LSP 준수 예제
interface Bird {
void fly();
}
interface Swimmer {
void swim();
}
class Penguin implements Bird, Swimmer {
@Override
public void fly() {
System.out.println("펭귄은 날지 못해요.");
}
@Override
public void swim() {
System.out.println("펭귄이 수영해요.");
}
}
public class Main {
public static void main(String[] args) {
Bird bird = new Penguin();
bird.fly(); // 예상 결과: 펭귄은 날지 못해요.
// 형변환 없이 인터페이스에 정의된 메서드 호출 가능
Swimmer swimmer = (Swimmer) bird;
swimmer.swim(); // 예상 결과: 펭귄이 수영해요.
}
}
Penguin
클래스가 Bird
와 Swimmer
인터페이스를 모두 구현하고 있습니다. 이렇게 하면 Penguin
객체를 Bird
인터페이스로 사용할 때 fly
메서드만 사용하고, Swimmer
인터페이스로 형변환하여 swim
메서드를 사용할 수 있습니다.Collection 인터페이스
Collection 타입의 객체에서 자료형을 LinkedList에서 전혀 다른 자료형 HashSet으로 바꿔도 add() 메서드를 실행하는데 있어 원래 의도대로 작동되기 때문에 LSP 원칙을 잘 지켰다 할 수 있다.
ISP 위배 예제
// 큰 인터페이스
interface Car {
void drive(); // 주행
void refuel(); // 주유
void enableAutonomousMode(); // 자율 주행 모드 전환
void disableAutonomousMode(); // 자융 주행 모드 해제
}
// 자동차 클래스 - 큰 인터페이스를 구현
class AutomatedCar implements Car {
@Override
public void drive() {
System.out.println("자동차가 주행합니다.");
}
@Override
public void refuel() {
System.out.println("자동차를 주유합니다.");
}
@Override
public void enableAutonomousMode() {
System.out.println("자동차가 자율 주행 모드로 전환합니다.");
}
@Override
public void disableAutonomousMode() {
System.out.println("자동차가 자율 주행 모드를 해제합니다.");
}
}
// 일반 차량 클래스 - 큰 인터페이스를 구현
class RegularCar implements Car {
@Override
public void drive() {
System.out.println("자동차가 주행합니다.");
}
@Override
public void refuel() {
System.out.println("자동차를 주유합니다.");
}
// 자율 주행을 지원하지 않는데 해당 메서드를 사용함
@Override
public void enableAutonomousMode() {
System.out.println("자율 주행 미지원 차량");
}
// 자율 주행을 지원하지 않는데 해당 메서드를 사용함
@Override
public void disableAutonomousMode() {
System.out.println("자율 주행 미지원 차량");
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
Car automatedCar = new AutomatedCar();
automatedCar.drive();
automatedCar.enableAutonomousMode();
automatedCar.refuel();
Car regularCar = new RegularCar();
regularCar.drive();
regularCar.enableAutonomousMode(); // 실제로는 사용되지 않아야 하는 메서드
regularCar.refuel();
}
}
AutomatedCar
)과 가능하지 않은 차량(RegularCar
)이 같은 기능을 가지고 있는 Car
인터페이스를 구현하고 있지만 RegularCar
는 enableAutonomousMode
와 disableAutonomousMode
가 필요 없음에도 메서드를 구현하고 있습니다. 이는 필요하지 않은 메소드를 의존하는 것으로 ISP 위반한 예제로 볼 수 있습니다.ISP 준수 예제
// 작은 인터페이스들
interface Drivable {
void drive(); // 주행
}
interface Refuelable {
void refuel(); // 주유
}
// 자율 주행을 지원하는 차량
interface AutonomousSupport {
void enableAutonomousMode(); // 자율 주행 모드 전환
void disableAutonomousMode(); // 자율 주행 모드 해제
}
// 자동차 클래스 - 필요한 작은 인터페이스들을 구현
class AutomatedCar implements Drivable, Refuelable, AutonomousSupport {
@Override
public void drive() {
System.out.println("자동차가 주행합니다.");
}
@Override
public void refuel() {
System.out.println("자동차를 주유합니다.");
}
@Override
public void enableAutonomousMode() {
System.out.println("자동차가 자율 주행 모드로 전환합니다.");
}
@Override
public void disableAutonomousMode() {
System.out.println("자동차가 자율 주행 모드를 해제합니다.");
}
}
// 자율 주행을 지원하지 않는 차량
class RegularCar implements Drivable, Refuelable {
@Override
public void drive() {
System.out.println("자동차가 주행합니다.");
}
@Override
public void refuel() {
System.out.println("자동차를 주유합니다.");
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// 자율 주행을 지원하는 차량
AutomatedCar autonomousCar = new AutomatedCar();
autonomousCar.drive();
autonomousCar.enableAutonomousMode();
autonomousCar.disableAutonomousMode();
autonomousCar.refuel();
// 자율 주행을 지원하지 않는 차량
RegularCar regularCar = new RegularCar();
regularCar.drive();
regularCar.refuel();
}
}
Drivable
, Refuelable
, AutonomousSupport
로 기능을 각각의 interface로 분리하여 인터페이스의 단일 책임 원칙을 지켰고, 클라이언트의 사용 목적과 용도에 맞는 기능만 구현 했기 때문에 ISP를 준수한 예제로 볼 수 있습니다.DIP 위배 예제
// 검 구현체
class Sword {
public void attackWithSword() {
System.out.println("검으로 공격합니다.");
}
}
// 활 구현체
class Bow {
public void attackWithBow() {
System.out.println("활로 공격합니다.");
}
}
// 게임 캐릭터 클래스 - 구체적인 무기 구현체에 직접 의존
class GameCharacter {
private Sword sword; // 구체적인 무기 타입에 직접 의존
public GameCharacter() {
this.sword = new Sword();
}
public void setSword(Sword sword) {
this.sword = sword;
}
public void attack() {
if (sword != null) {
sword.attackWithSword();
} else {
System.out.println("무기가 없어서 공격할 수 없습니다.");
}
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// 캐릭터 생성
GameCharacter character = new GameCharacter();
// 검으로 공격
character.attack();
// 활로 무기 변경 후 공격 (DIP 위반)
character.setSword(new Bow()); // DIP 위반: 구체적인 무기 구현체에 직접 의존
character.attack(); // 이 부분에서 문제가 발생할 수 있음
}
}
GameCharacter
클래스는 Sword
클래스에 직접 의존하고 있습니다. 이렇게 되면 새로운 무기를 추가하거나 변경하려면 GameCharacter
클래스의 코드를 수정해야 하므로 DIP를 위반하는 예시입니다. DIP 준수 예제
// 무기 인터페이스
interface Weapon {
void attack();
}
// 검 구현체
class Sword implements Weapon {
@Override
public void attack() {
System.out.println("검으로 공격합니다.");
}
}
// 활 구현체
class Bow implements Weapon {
@Override
public void attack() {
System.out.println("활로 공격합니다.");
}
}
// 게임 캐릭터 클래스 - Weapon에 의존
class GameCharacter {
private Weapon weapon;
public GameCharacter(Weapon weapon) {
this.weapon = weapon;
}
public void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
public void attack() {
if (weapon != null) {
weapon.attack();
} else {
System.out.println("무기가 없어서 공격할 수 없습니다.");
}
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// 캐릭터 생성
GameCharacter character = new GameCharacter(new Sword());
// 검으로 공격
character.attack();
// 활로 무기 변경 후 공격
character.setWeapon(new Bow());
character.attack();
}
}
GameCharacter
클래스는 Weapon
인터페이스에 의존하고 있습니다. 이렇게 함으로써 GameCharacter
클래스는 구체적인 무기 구현에 의존하는 것이 아니라, Weapon
인터페이스에 의존하게 되어 새로운 무기를 추가하거나 변경할 때 기존 코드를 수정하지 않고 확장할 수 있습니다.1) 코드의 재사용성 용이
- 상속을 통해 프로그래밍 시, 코드의 재사용률을 높일 수 있습니다.
2) 생산성 향상
- 잘 설계된 클래스를 통해 독립적인 객체를 활용함으로써, 개발의 생산성을 향상할 수 있습니다.
3) 자연적인 모델링
- 일상생활에서 쓰는 개념을 그대로 객체라는 구조로 표현하여 개발함으로써, 생각한것을 그대로 구현할 수 있습니다.
4) 유지보수의 용이성
- 프로그램을 변경할 때 수정, 추가를 하더라도 캡슐화를 통해 주변 코드에 영향이 적기 때문에 유지보수가 용이합니다.
1) 실행 속도가 느림.
- 객체지향언어(C++, JAVA 등)는 상대적으로 실행 속도가 느립니다.
2) 프로그램 용량이 큼
- 객체 단위로 프로그램을 많이 만들다보면, 불필요한 정보들이 같이 삽입될 수 있고, 이는 프로그램의 용량 증가로 이어질 수 있습니다.
3) 설계에 많은 시간 소요
- 클래스별로, 객체별로 설계하고, 상속 등의 구조 또한 설계하여야 하기 때문에, 설계단계부터 많은 시간이 소모됩니다.
참고