SOLID의 모든 원칙은 결국 "추상화"로 수렴한다.
1주일 동안의 강의 내용
결론부터 말하자면 SOLID원칙 모두 구체적인 구현에 의존하지말고 추상화를 통해 유연한 설계를 하라는 점에서 공통된다.
1⃣ OCP (개방-폐쇄 원칙: Open-Closed Principle)
“확장은 가능하지만, 기존 코드는 수정하지 말것”
새로운 기능을 추가할 때 기존 코드를 변경하지 않고, 새로운 기능을 담당하는 객체를 생성하고 새로운 기능과, 기존 기능을 확장할 수 있도록 인터페이스를 활용하여 설계를 가능하게 해야한다.
✅ 추상화 적용 → Shape 인터페이스를 만들고 Circle, Rectangle 등이 이를 구현하면 Shape을 확장해도 기존 코드 변경 없이 새로운 도형을 추가할 수 있음.
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
}
class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) { this.width = width; this.height = height; }
public double area() { return width * height; }
}
// 기존 코드 수정 없이 새로운 도형 추가 가능
해당 부분에서 Triangle과 같은 새로운 기능이 추가 되어도, 기존의 코드를 수정하는 것이 아닌 Shape를 구현할 수 있게 해주면 된다.
2⃣ ISP (인터페이스 분리 원칙: Interface Segregation Principle)
“인터페이스는 클라이언트가 사용하지 않는 기능을 강요해서는 안된다”
하나의 거대한 인터페이스, 즉 하나의 개념에 대해서 그 개념을 모두 통괄할 수 있는 인터페이스를 만드는 것이 아닌 역할별로 분리된 작은 인터페이스를 설계해야 한다.
interface Bird {
void fly();
}
class ButterFly implements Bird {
void fly() {
System.out.println("fly");
}
}
class Penguin implements Bird {
void fly() {
System.out.println("can't fly...only walk");
}
}
✅ 현재 Penguin은 날지 못하지만 Bird란 추상적인에 속해 fly를 강요받는다…
✅ 추상화 적용 → 여러 개의 인터페이스로 분리하여 불필요한 의존성을 줄이고, 유연한 설계 가능.
interface Fly {
void fly();
}
interface Walk {
void walk();
}
이렇게 기능을 작은 단위의 인터페이스로 쪼개서 사용하면 된다.
3⃣ LSP (리스코프 치환 원칙: Liskov Substitution Principle)
“하위 클래스는 상위 클래스의 기능을 대체할 수 있어야 한다”
상위 클래스 타입으로 하위 클래스를 사용해도 정상적으로 동작해야 한다. 이를 위해 추상화된 부모 클래스를 만들고 이를 확장하는 하위 클래스 들이 부모의 행위를 그대로 유지해야 한다.
✅ 추상화 적용 → 하위 클래스는 상위 클래스의 동작을 유지해야 함.
class Bird {
void fly() { System.out.println("Flying..."); }
}
class Sparrow extends Bird { } // 정상적으로 동작
class Ostrich extends Bird {
@Override
void fly() { throw new UnsupportedOperationException("I can't fly!"); } // LSP 위반
}
✅ Ostrich는 Bird이지만 날 수 없음 → fly()를 강제하는 구조 자체가 잘못됨
✅ 이를 해결하려면 Flyable 인터페이스를 분리하여, 날 수 있는 새만 이를 구현하도록 해야 함
4⃣ DIP (의존성 역전 원칙: Dependency Inversion Principle)
“상위 모듈이 하위 모듈에 의존하는 것이 아니라 둘 다 추상화(인터페이스)에 의존해야 한다.”
즉 구체적인 클래스가 아니라 인터페이스나 추상 클래스를 사용하여 의존성을 주입받아야 한다.
interface Repository {
void save();
}
class MySQLRepository implements Repository{
void save() {
System.out.println("MySQL Save Processing");
}
}
class JPARepository implements Repository {
void save();
}
Repository라는 추상화를 적용시켜서 이를 구현한 구현체로 MySQL, JPA, MongoDB… 새로운 DB가 추가 되어도 구현체에 직접 의존하는 것이 아닌 상위 모듈을 의존성 주입받아 런타임에 구현체를 동적으로 변경할 수 있도록 구현한다.
결론: 모든 원칙이 “추상화”로 수렴한다.
📌 OCP (개방-폐쇄 원칙) → 추상화(인터페이스)를 이용하면 기존 코드를 수정하지 않고도 확장 가능
📌 ISP (인터페이스 분리 원칙) → 불필요한 의존성을 제거하고, 필요한 기능만 제공하는 인터페이스 설계
📌 LSP (리스코프 치환 원칙) → 추상화를 잘못하면 하위 클래스가 부모 클래스를 대체하지 못함 → 올바른 추상화가 중요
📌 DIP (의존성 역전 원칙) → 구체적인 구현이 아니라 추상화(인터페이스)에 의존해야 함.
📌 SRP (단일 책임 원칙) → 클래스는 단 하나의 책임만을 가지고 있어야 하고, 해당 클래스가 변경될 경우 그 이유는 오로지 책임에 의한 것이어야 함. → SRP 또한 각 클래스가 하나의 역할만 수행하므로 자연스럽게 추상화가 이루어지고, 다른 SOLID 원칙들고 적용하기 쉬워진다.
1주일 동안의 회고
깊은 내공이 느껴지는 강의를 듣고 과연 나의 코드는 읽기 좋은 코드는 못 될지 언정 과연 읽을만 한가?라고 생각하며 여태 했던 개인프로젝트들과 팀 프로젝트의 코드들을 살펴보았고, 이번 일주일 동안 강의에서 배운 SOLID원칙을 활용한 추상과 구체, 논리와 사고의 흐름 및 사고의 깊이 줄이기 같은 요소들을 하나둘씩 살펴보았다.
긴 시간은 투자하지 못했지만 OCP와 DIP, SRP 원칙만큼은 쉽게 눈에 보이는 레벨에서 고쳐나갈 수 있다고 생각하고 나름 만족할 만큼 적용했다고 생각한다.
나는 개인적으로 객체지향이라는 개념 자체가 너무 추상화되어있는 표현 혹은 단어라고 생각한다. 이것에 대한 개념에 대해서 알고있어도 초보자 레벨에서 이를 적용시키기란 매우 어렵고, 사람마다 그 기준이 너무나도 다르기 때문에 정답은 없는 것 같다라는 생각이 든다. 정답은 없지만 정답에 가까운 그런 방식은 있을 것이라고 생각한다.
나는 사실 내가 객체지향을 초보자 레벨에서는 나름 잘 적용해왔다고 생각해 왔었다. 그 이유는 우테코 프리코스를 2번이나 해보면서(2번 다 떨어졌다…) 그리고 실제로 친한 중고등학교 친구가 배민에서 개발자로 일하기 때문에 친구가 일주일에 한번씩 코드리뷰도 해주었었다.(많이 혼났었지만…) 나름 객체지향이라는 개념과 SRP, OCP, DIP라는 개념은 실무에서도 실제로 가장 강력한 무기가 될 수 있기에 잘 지켜왔었다고 생각한다.
결론은 “추상화”가 가장 중요한데.. 이부분이 제일 어렵다.
그래서 코드를 작성할 때 이 부분만큼은 옆에 메모장에 띄워놓고 익숙해질 때까지 해야한다 라고 생각한다.
1. 코드는 적절한 수준에서 추상화되어야 한다.
핵심 로직을 메서드 또는 클래스로 분리하여 한 가지 역할에 집중하도록 한다.
코드의 유연성과 확장성을 높이기 위해서는 인터페이스와 구현을 분리해야한다.
네이밍을 통해 역할을 명확하게 전달한다
2.논리와 사고의 흐름
코드는 논리적으로 자연스럽게 읽혀야 한다.
코드의 동작 순서가 사람이 생각하는 방식과 유사해야 한다.
조건문과 루프의 배치를 신중히 고려해야 한다.
불필요한 상태 변경을 줄여야한다.
여기서 사고의 깊이가 가속화 된다. 상태가 변경되는 시점을 기준으로 변경된걸 인치한 채 코드를 따라가야 한다.
3. 사고의 깊이 줄이기
한번에 이해할 수 있는 코드가 가장 좋은 코드다
Simple is best!!!
조건문을 단순화 하여라.
그리고 부정문(!)을 사용할 경우 메서드명으로 그 의도를 명확히 나타내는쪽으로 코드를 작성해라
이미 여기서 상태의 변경이 일어난 상태로 if문 안의 로직을 들어간다.
메서드를 작게 나눠라.
작게 나누지 못할 경우 추상화 혹은 다른 클래스의 책임을 나누는 것을 고려하라.
분명 나는 SRP원칙을 잘 지켰다고 생각한 상태에서 좀 두꺼운 메서드를 나누려고 할 때 나누지 못하겠다면 SRP원칙을 어긴건 아닌지 다시 보자.
Early Return 패턴을 통해 중첩을 줄이자.
결론
적절한 추상화 → 코드가 마치 살아있고 객체지향적인 코드가 나타난다. 이는 코드의 유연성을 높인다.
논리적인 흐름 → 코드를 읽을 때 직관적인 이해를 돕기 때문에 유지보수하기 편하고, 소프트웨어의 품질을 높일 수 있다.
사고의 깊이 줄이기 → 코드를 한번에 쉽게 파악할 수 있도록 만들기 때문에 유지 보수 및 기능 추가 등등의 비용을 줄일 수 있다.
Day2, Day4 미션에 대한 회고
Day2 주제 : 추상과 구체에 대한 예시 3~5문장으로 정리하기.
[해결 과정]
추상과 구체를 어떻게 정의할 것인가?
“추상이란 여러 개념을 일반화 하여 표현한 것이며, 구체는 해당 개념을 세부적으로 설명하는 것이다” 라는 사전적인 정의를 먼저 내가 스스로 이해하고 접근 하였다.
[접근 방식]
일상에서의 추상과 구체의 예시를 찾아 정리하였다.
일상 생활에서의 추상
추상 → 동물
구체 → 개, 고양이, 새, 쥐 … 등등
의문 → 개 또한 추상적인 개념이 아닌가….?? 더 나아가서 “진돗개” , “시츄”, “셰퍼드”, “포메라니안”… 등등
배운점 : 어디까지가 적절한 추상화인가….??에 대해서 깊이 고민하게 되었다…
Day4 주제 : 코드 리팩토링, SOLID에 대한 자기만의 언어로 정리.
[해결과정]
사고의 깊이 줄이기
이번 미션으로 내준 코드는 if ~ esle 그 내부에 또다른 if와 else..
객체지향적인 사고
if 조건문의 로직 변경하기
[접근 방식]
복잡한 if문에서 빠져나오는 것이 첫번째라 생각했고, 이는 early return으로 쉽게 해결할 수 있다.
getter를 통해 해당 객체의 외부 정보를 빼내와서 외부의 정보와 데이터를 가공하는 로직이 직접적으로 들어나있는 부분들을 객체 내부로 숨기고 캡슐화와 객체의 역할을 정확히 하고 나누는 방향으로 접근했다.
그렇게 했을 때 책임과 역할이 객체간에 명확하게 되고, 캡슐화를 지킴으로써 유지보수성이 올라간다고 생각했다.
댓글을 작성해보세요.