출처: https://www.youtube.com/watch?v=7c0tqHLfxlE&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=101
SOLID: 객체지향 설계에서 유지보수성과 확장성을 높이기 위한 설계 원칙
SRP 단일 책임 원칙
- 객체는 한 가지 역할(책임)만 가져야 한다.
- 객체가 변경되는 이유는 단 한 가지여야 한다.
- 필요성: 책임이 변경의 축이기 때문에 분할되는 것이 중요
- 책임이 여러 개면 클래스가 커지고 책임 간의 결합도가 높아져 연쇄적인 변화가 발생
- 어떻게?
- 추상화를 통해 객체를 설계할 때 한 개의 역할만 갖도록 구성
- 책임을 묶어서 생각하기 때문에 쉽지 않음... -> 책임의 분배는 설계 상황에 따라 바뀔 수 있으므로 경험이 중요
OCP 개방 폐쇄 원칙
- 객체의 확장에 열려 있고 변경에는 닫혀 있어야 한다.
- 확장에 열려 있다. -> 행위를 추가해 객체가 하는 일을 확장할 수 있다.
- 변경에 닫혀 있다. -> 객체의 확장이 소스 코드의 변경을 초래하지 않는다.
- 기본 코드를 쉽게 확장 가능하므로 유연성, 재사용성, 유지보수성을 얻음
- 어떻게?
- 추상화 -> 모듈이 잘 변하지 않는 추상화에 의존해 닫혀있고, 구현체를 추가해 확장에는 열려 있도록

LSP 리스코프 치환 원칙
- 서브 타입은 자신의 기반 타입으로 교체할 수 있어야 한다.
- 하위 클래스가 상위 클래스로 바뀌어도 역할 수행에 문제가 없어야 한다.
- 서브 타입은 기반 타입이 정해둔 약속을 지켜야 한다.
- 리스코프 치환 원칙의 위반 -> 개방 폐쇄 원칙의 위반
- 어떻게?
- 하위 클래스 is-a 상위 클래스
- 구현 클래스 is able to 인터페이스
- 행위에 대한 is-a가 잘 지켜져야 한다.
ISP 인터페이스 분리 원칙
- 인터페이스는 자신의 클라이언트가 사용할 메서드만 가지고 있어야 한다.
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하게 되면 안된다.
- 인터페이스가 커질 수록 같은 인터페이스를 구현하는 클라이언트간 결합도가 높아짐
- 어떻게?
- 자신의 클라이언트가 필요로 하는 함수만 선언
- 인터페이스 구현은 is-able-to로 해석 -> 가능하게 만들기 위해 필요한 기능만 제공
- 예시) Comparable 인터페이스는 구현체가 비교 가능한 객체가 되기 위해 필요한 기능만 제공
DIP 의존 관계 역전 원칙
- 구체적인 것이 추상화된 것에 의존해야 한다.
- 자주 변경되는 것에 의존하지 말아라
- 상위 객체는 본질을 담고 정책을 결정하는 역할
- 추상화가 구체적인 것에 의존하면 자유롭게 재사용할 수 없음
- 어떻게?
- 의존성은 이행적 -> 하위 수준에 변화가 생기면 연쇄적으로 변화가 발생
- 동일 레벨에서는 추상화를 의존, 하위 레벨이 상위 레벨을 의존하게 만들어 역전


추가 정리
객체지향의 4가지 특성
- 캡슐화, 상속, 추상화, 다형성
- 오버로딩: 같은 이름을 가진 메소드나 생성자를 여러 개 정의하는 것
- 매개변수의 타입, 순서 또는 개수가 다를 때 사용됨
- 정적 다형성을 지원
- 컴파일 시점에 어떤 메소드가 호출될지 결정됨
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// int 형식의 add 메소드 호출 (정적 다형성)
int result1 = calculator.add(5, 3);
// double 형식의 add 메소드 호출 (정적 다형성)
double result2 = calculator.add(2.5, 3.7);
}
}
- 오버라이딩:상위 클래스에서 정의된 메서드를 하위 클래스에서 동일한 메서드 시그니처를 가지고 재정의하는 것
- 런타임 다형성을 지원
- @Override: 컴파일러에서 메소드 오버라이딩의 정확성을 검증하고 규칙을 준수하는지 확인 -> 오류 방지
class Animal {
void makeSound() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("왈왈! 크르릉..!");
}
}
public class Main {
public static void main(String[] args) {
// 다형성을 이용한 상위 클래스 참조 변수로 하위 클래스 객체를 참조
Animal myPet = new Dog();
// Dog 클래스의 makeSound 메소드가 호출됨 (런타임 다형성)
myPet.makeSound();
}
}
출력결과
왈왈! 크르릉..!
SOLID를 배우는 이유
- 역할(인터페이스)과 구현(인터페이스를 구현한 객체)으로 구분하자.
- 역할, 구현이라는 관점을 다형성을 통해 객체로 만들 수 있다.
- 유연하고 변경이 용이하다.
- 다형성의 한계: 역할이 변하게 되면 모두에 큰 변경이 발생 -> 인터페이스를 안정적으로 설계하는 것이 중요
- 이를 위해 SOLID를 알아야 함
스프링과 객체 지향
- 스프링은 다형성을 극대화하여 이용할 수 있도록 도와줌
- 제어의 역전(IOC), 의존관계 주입(DI)을 다형성을 활용해서 편리하게 다룰 수 있도록 지원
단일 책임 원칙
- 코드의 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 지킨 것
// 단일 책임 원칙을 지킨 예
class Reservation {
public void reserveRoom() {
// 방 예약 로직
}
}
class Payment {
public void processPayment() {
// 결제 처리 로직
}
}
// 단일 책임 원칙을 위반한 예
// 예약과 결제, 두 가지 다른 책임을 가집니다. 따라서 이 클래스는 SRP를 위반하고 있습니다.
class ReservationAndPayment {
public void reserveRoom() {
// 방 예약 로직
}
public void processPayment() {
// 결제 처리 로직
}
}
개방-폐쇄 원칙
- 인터페이스를 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
- 잘못된 예제
- 기능 추가 시 기존 코드를 변경해야 한다.
class Shape {
public void drawCircle() {
System.out.println("원 그리기");
}
public void drawRectangle() {
System.out.println("사각형 그리기");
}
}
public class Main {
public static void main(String[] args) {
Shape shape = new Shape();
shape.drawCircle();
// shape.drawRectangle();
}
}
- 다형성을 활용한 예제
- 문제점: 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("원 그리기");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("사각형 그리기");
}
}
public class Main {
public static void main(String[] args) {
Shape circle = new Circle();
circle.draw();
Shape rectangle = new Rectangle();
rectangle.draw();
}
}
- 의존성 주입 방법
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("원 그리기");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("사각형 그리기");
}
}
class ShapeRenderer {
public void draw(Shape shape) {
shape.draw();
}
}
public class Main {
public static void main(String[] args) {
ShapeRenderer shape = new ShapeRenderer();
shape.draw(new Circle());
shape.draw(new Rectangle());
}
}
리스코프 치환 법칙
- 인터페이스 하위 클래스는 반드시 규약을 지켜야 함
- 인터페이스를 구현한 구현체를 믿고 사용하기 위해 필요
- 위반한 코드: 하위 클래스가 상위 클래스를 대체 불가능한 경우
class Car {
void startEngine() {
// 엔진 시작 동작을 구현
}
}
class ElectricCar {
void chargeBattery() {
// 배터리 충전 동작을 구현
}
}
// 하이브리드!? 충전 해야하는데..?
class HybridCar extends Car {
// ...
}
- 해결 방법
1. 인터페이스를 활용
interface ElectricChargeable {
void chargeBattery();
}
class Car {
void startEngine() {
// 엔진 시작 동작을 구현
}
}
class ElectricCar extends Car implements ElectricChargeable {
@Override
public void chargeBattery() {
// 배터리 충전 동작을 구현
}
}
class HybridCar extends Car implements ElectricChargeable {
@Override
public void chargeBattery() {
// 배터리 충전 동작을 구현
}
}
2. 추상 클래스 사용
abstract class Car {
void startEngine() {
// 엔진 시작 동작을 구현
}
abstract void additionalAction(); // 추가 동작을 서브클래스에서 구현
}
class ElectricCar extends Car {
@Override
void additionalAction() {
// 배터리 충전 동작을 구현
}
}
class HybridCar extends Car {
@Override
void additionalAction() {
// 배터리 충전 동작을 구현
}
}
- 잘 지킨 코드
interface Shape {
double getArea();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
인터페이스 분리 원칙
- 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스로 분리하자
- 인터페이스가 명확해지고 대체 가능성이 높아진다.
// 인터페이스 정의
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
// Worker 클래스가 필요한 동작을 구현
class Worker implements Workable, Eatable, Sleepable {
@Override
public void work() {
System.out.println("근무 중");
}
@Override
public void eat() {
System.out.println("식사 중");
}
@Override
public void sleep() {
System.out.println("휴식 중");
}
}
// Manager 클래스는 클라이언트가 필요로 하는 동작을 사용
class Manager {
public void assignWork(Workable worker) {
System.out.print("일 할당: ");
worker.work();
}
public void provideLunch(Eatable worker) {
System.out.print("점심 제공: ");
worker.eat();
}
public void allowRest(Sleepable worker) {
System.out.print("휴식 허용: ");
worker.sleep();
}
}
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
Manager manager = new Manager();
// 클라이언트가 필요로 하는 동작을 호출
manager.assignWork(worker);
manager.provideLunch(worker);
manager.allowRest(worker);
}
}
의존관계 역전 원칙
- 의존성 주입은 이 원칙을 따르는 방법
- 변하지 않을 가능성이 높은 인터페이스(역할)에 의존
interface Engine {
void start();
}
class GasolineEngine implements Engine {
public void start() {
System.out.println("Gasoline engine started.");
}
}
class ElectricEngine implements Engine {
public void start() {
System.out.println("Electric engine started.");
}
}
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine; // 추상화된 인터페이스에 의존
}
public void startEngine() {
engine.start();
}
}
'Spring & JPA' 카테고리의 다른 글
스프링 부트 특징과 Spring Bean, Spring MVC (0) | 2025.04.13 |
---|---|
[테코톡] Lock & JPA Lock (0) | 2025.04.05 |
[테코톡] JPA 연관관계 최적화 (0) | 2025.04.03 |
[테코톡] API 중복 호출 해결기 (0) | 2025.04.02 |
고급 매핑 - 3. 복합 키와 식별 관계 매핑 (0) | 2025.03.19 |