-
코드잇 백엔드 스프린트 - 4주차 워클리 페이퍼 : SOLID 원칙개발/활동 2025. 3. 3. 22:19반응형
위클리 페이퍼는 이론 수업에서 배운 내용에 관련된 특정 주제에 대하여 심화 학습을 하고, 학습한 내용을 문서로 작성하는 과제입니다.
- 객체지향 프로그래밍 설계원칙에서 '단일 책임 원칙(SRP)'과 '개방-폐쇄 원칙(OCP)'에 대해 설명하고, 각각의 원칙을 적용한 코드 예시를 들어주세요.
Java로 SOLID 원칙
각 원칙마다 잘못된 예시(위반)와 개선된 코드(적용)를 비교하며 설명할게! 🚀
1️⃣ SRP (Single Responsibility Principle) - 단일 책임 원칙
"하나의 클래스는 하나의 책임만 가져야 한다."
❌ 잘못된 예시 (SRP 위반)
class 직원 { private String 이름; private double 급여; public 직원(String 이름, double 급여) { this.이름 = 이름; this.급여 = 급여; } public void 급여계산() { // ✅ 급여 계산 역할 System.out.println("급여를 계산합니다..."); } public void 데이터저장() { // ❌ 데이터 저장 역할도 포함 (SRP 위반) System.out.println("직원 정보를 데이터베이스에 저장합니다."); } }🚨 문제점
- 직원 클래스가 "급여 계산"과 "데이터 저장" 두 가지 역할을 담당하고 있음 → 단일 책임 원칙 위반
- 급여 계산 로직이 바뀔 때, 데이터 저장 코드도 영향을 받을 가능성이 있음
✅ 개선된 코드 (SRP 적용)
// 직원 클래스 (데이터만 저장) class 직원 { private String 이름; private double 급여; public 직원(String 이름, double 급여) { this.이름 = 이름; this.급여 = 급여; } } // 급여 계산 클래스 (급여 관련 로직 분리) class 급여계산기 { public void 급여계산(직원 직원) { System.out.println("급여를 계산합니다..."); } } // 직원 저장 클래스 (데이터 저장 역할 분리) class 직원저장소 { public void 데이터저장(직원 직원) { System.out.println("직원 정보를 데이터베이스에 저장합니다."); } }🎯 개선점
✅ 급여 계산과 데이터 저장을 각각 다른 클래스로 분리하여 책임을 명확히 함
✅ 특정 기능(급여 계산, 데이터 저장)에 변경이 발생해도 서로 영향을 주지 않음
2️⃣ OCP (Open/Closed Principle) - 개방/폐쇄 원칙
"확장에는 열려 있고, 기존 코드 수정에는 닫혀 있어야 한다."
❌ 잘못된 예시 (OCP 위반)
class 결제처리기 { public void 결제(String 결제방식, double 금액) { if (결제방식.equals("신용카드")) { System.out.println("신용카드로 " + 금액 + "원을 결제합니다."); } else if (결제방식.equals("페이팔")) { System.out.println("PayPal로 " + 금액 + "원을 결제합니다."); } } }🚨 문제점
- 새로운 결제 방식(예: 카카오페이)을 추가할 때마다 if-else 문을 계속 추가해야 함 → 확장에 닫혀 있음
- 기존 코드(결제처리기)를 수정해야 함 → 유지보수 어려움
✅ 개선된 코드 (OCP 적용 - 다형성 활용)
// 결제방식 인터페이스 interface 결제방식 { void 결제(double 금액); } // 신용카드 결제 클래스 class 신용카드결제 implements 결제방식 { public void 결제(double 금액) { System.out.println("신용카드로 " + 금액 + "원을 결제합니다."); } } // PayPal 결제 클래스 class 페이팔결제 implements 결제방식 { public void 결제(double 금액) { System.out.println("PayPal로 " + 금액 + "원을 결제합니다."); } } // 결제 처리 클래스 (OCP 적용) class 결제처리기 { private 결제방식 방식; public 결제처리기(결제방식 방식) { this.방식 = 방식; } public void 결제진행(double 금액) { 방식.결제(금액); } } // 사용 예시 public class 메인 { public static void main(String[] args) { 결제처리기 처리기 = new 결제처리기(new 신용카드결제()); 처리기.결제진행(1000); 처리기 = new 결제처리기(new 페이팔결제()); 처리기.결제진행(500); } }🎯 개선점
✅ 새로운 결제 방식이 추가될 때도 기존 코드 수정 없이 확장 가능
✅ if-else 조건문 제거 → 코드 가독성이 좋아짐
3️⃣ LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
"자식 클래스는 부모 클래스를 대체할 수 있어야 한다."
❌ 잘못된 예시 (LSP 위반 - 펭귄이 날지 못함)
class 새 { public void 날다() { System.out.println("새가 날아갑니다!"); } } class 펭귄 extends 새 { @Override public void 날다() { // ❌ LSP 위반 (펭귄은 날지 못함) throw new UnsupportedOperationException("펭귄은 날 수 없습니다!"); } }🚨 문제점
- 펭귄은 새를 상속받았지만 날다() 기능을 사용할 수 없음 → LSP 위반
- 펭귄을 새로 사용하려고 하면 예외가 발생함 → 프로그램 안정성 저하
✅ 개선된 코드 (LSP 적용 - 인터페이스 분리)
// 새 인터페이스 interface 새 { void 먹다(); } // 날 수 있는 새 인터페이스 interface 날수있는새 { void 날다(); } // 참새 (날 수 있음) class 참새 implements 새, 날수있는새 { public void 먹다() { System.out.println("참새가 먹이를 먹습니다."); } public void 날다() { System.out.println("참새가 날아갑니다."); } } // 펭귄 (날 수 없음) class 펭귄 implements 새 { public void 먹다() { System.out.println("펭귄이 물고기를 먹습니다."); } }🎯 개선점
✅ 새와 날 수 있는 새를 분리하여 구조를 개선
✅ 펭귄이 날다() 메서드를 상속받지 않으므로 예외 발생 방지4️⃣ ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
"클라이언트(사용자)는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다."
(즉, 하나의 거대한 인터페이스보다는 여러 개의 작은 인터페이스로 나누는 것이 좋다.)
❌ 잘못된 예시 (ISP 위반 - 로봇은 eat() 필요 없음)
// 작업 인터페이스 (모든 작업자가 반드시 구현해야 함) interface 작업자 { void 작업하다(); void 식사하다(); } // 인간 직원 (문제 없음) class 인간직원 implements 작업자 { public void 작업하다() { System.out.println("일을 하고 있습니다."); } public void 식사하다() { System.out.println("밥을 먹고 있습니다."); } } // 로봇 직원 (문제 발생!) class 로봇직원 implements 작업자 { public void 작업하다() { System.out.println("로봇이 작업을 수행합니다."); } public void 식사하다() { // ❌ 로봇은 식사할 필요 없음! throw new UnsupportedOperationException("로봇은 밥을 먹지 않습니다!"); } }🚨 문제점
- 작업자 인터페이스가 모든 작업자는 반드시 식사하다()를 구현해야 한다고 강요함.
- 로봇은 밥을 먹지 않지만, 어쩔 수 없이 식사하다()를 구현해야 함.
- 불필요한 메서드를 구현하게 되어 유지보수가 어려워짐.
✅ 개선된 코드 (ISP 적용 - 인터페이스 분리)
// 작업 가능 인터페이스 (작업과 관련된 역할) interface 작업가능 { void 작업하다(); } // 식사 가능 인터페이스 (식사가 필요한 경우만) interface 식사가능 { void 식사하다(); } // 인간 직원 (작업도 하고 식사도 함) class 인간직원 implements 작업가능, 식사가능 { public void 작업하다() { System.out.println("일을 하고 있습니다."); } public void 식사하다() { System.out.println("밥을 먹고 있습니다."); } } // 로봇 직원 (작업만 함) class 로봇직원 implements 작업가능 { public void 작업하다() { System.out.println("로봇이 작업을 수행합니다."); } }🎯 개선점
✅ 작업과 식사를 별도의 인터페이스로 분리하여 유연성을 높임
✅ 로봇은 작업가능 인터페이스만 구현하면 되므로, 불필요한 식사하다() 메서드를 구현할 필요 없음
✅ 각 객체가 필요한 기능만 가지도록 설계하여 유지보수가 쉬워짐
5️⃣ DIP (Dependency Inversion Principle) - 의존성 역전 원칙
"고수준 모듈(상위 클래스)은 저수준 모듈(하위 클래스)에 의존하지 않고, 추상화에 의존해야 한다."
(즉, 구체적인 클래스가 아니라 인터페이스에 의존하도록 설계해야 한다.)
❌ 잘못된 예시 (DIP 위반 - 직접적인 의존성 문제)
class 전구 { public void 전원켜기() { System.out.println("전구가 켜졌습니다."); } } class 스위치 { private 전구 장치; // ❌ DIP 위반 (구체적인 클래스에 의존) public 스위치() { this.장치 = new 전구(); } public void 작동하다() { 장치.전원켜기(); } }🚨 문제점
- 스위치 클래스가 전구 클래스에 직접 의존하고 있음.
- 다른 장치를 사용하고 싶다면(예: 선풍기), 스위치 클래스를 수정해야 함 → 유지보수 어려움.
- 변경에 유연하지 않고 결합도가 높음.
✅ 개선된 코드 (DIP 적용 - 인터페이스 활용)
// 전원을 켤 수 있는 장치 인터페이스 interface 전원가능 { void 전원켜기(); } // 전구 클래스 class 전구 implements 전원가능 { public void 전원켜기() { System.out.println("전구가 켜졌습니다."); } } // 선풍기 클래스 (새로운 장치 추가) class 선풍기 implements 전원가능 { public void 전원켜기() { System.out.println("선풍기가 작동합니다."); } } // 스위치 (전원가능 인터페이스에 의존) class 스위치 { private 전원가능 장치; // ✅ DIP 적용 (추상화에 의존) public 스위치(전원가능 장치) { this.장치 = 장치; } public void 작동하다() { 장치.전원켜기(); } } // 사용 예시 public class 메인 { public static void main(String[] args) { 스위치 전구스위치 = new 스위치(new 전구()); 전구스위치.작동하다(); // 전구가 켜졌습니다. 스위치 선풍기스위치 = new 스위치(new 선풍기()); 선풍기스위치.작동하다(); // 선풍기가 작동합니다. } }🎯 개선점
✅ 스위치가 전구가 아니라 전원가능 인터페이스에 의존하도록 설계
✅ 새로운 장치(예: 선풍기) 추가 시, 스위치 코드를 수정할 필요 없음
✅ 코드의 유연성이 높아지고, 유지보수가 쉬워짐
🔥 SOLID 원칙 정리표
원칙 잘못된 점 개선 방법
SRP (단일 책임 원칙) 하나의 클래스가 너무 많은 역할을 가짐 역할을 분리하여 독립적인 클래스로 설계 OCP (개방/폐쇄 원칙) 기능 추가 시 기존 코드 수정 필요 인터페이스와 다형성을 활용하여 확장성 확보 LSP (리스코프 치환 원칙) 자식 클래스가 부모 클래스를 대체할 수 없음 적절한 인터페이스 설계를 통해 계층 분리 ISP (인터페이스 분리 원칙) 인터페이스가 너무 많은 기능을 강요 필요한 기능만 포함한 작은 인터페이스로 분리 DIP (의존성 역전 원칙) 구체적인 클래스에 직접 의존 인터페이스를 통해 의존성을 역전
반응형'개발 > 활동' 카테고리의 다른 글
코드잇 백엔드 스프린트 - 7주차 위클리-페이퍼 WAS (0) 2025.03.24 코드잇 백엔드 스프린트 - 6주차 위클리-페이퍼 : 프레임워크와 라이브러리의 차이점 (0) 2025.03.16 코드잇 백엔드 스프린트 - 6주차 위클리-페이퍼 : Spring Framework의 탄생 배경과 해결하고자 했던 문제점 (0) 2025.03.16 코드잇 백엔드 스프린트 - 5주차 위클리 페이퍼 : HashSet 와 O(n)과 O(log n)의 성능 차이 (0) 2025.03.09 코드잇 백엔드 스프린트 - 4주차 위클리-페이퍼 : 특정 함수변환 (0) 2025.03.03