[ 4주차 - 0905 ]
금일 커리큘럼
├ 09:00 ~ 12:00 자바 프로그래밍 기초 (객체지향 프로그래밍 OOP, SOLID 원칙, SRP)
└ 13:00 ~ 18:00 자바 프로그래밍 기초 (OCP, LSP, ISP, DIP)
1. 객체지향 프로그래밍 (OOP)
Object-Oriented Programming
- 추상화 (abstraction)
- 복잡한 현실 세계의 객체에서 핵심 특징만 뽑아내는 것
- 불필요한 세부 사항은 감추고 중요한 속성과 동작만 모델링
- 예: 자동차 객체 → 엔진, 바퀴, 운전 기능만 정의 (제작 공정, 나사 개수 등은 제외)
- 캡슐화 (Encapsulation)
- 데이터(필드)와 기능(메서드)을 하나의 클래스 안에 묶고, 외부 접근을 제한
- 접근 제어자(private, public 등)를 통해 정보 은닉
- 예: 은행 계좌 클래스 → 계좌비밀번호는 private으로 숨기고, 입출금은 public 메서드로만 가능
- 상속 (Inheritance)
- 기존 클래스(부모)의 속성과 기능을 새로운 클래스(자식)가 물려받는 것
- 코드 재사용성과 확장성 향상
- 예: 동물 → 포유류 → 인간
- 다형성 (Polymorphism)
- 같은 이름의 메서드나 객체가 상황에 따라 다르게 동작하는 성질
- 오버로딩: 같은 메서드 이름, 매개변수만 다름
- 오버라이딩: 부모 메서드를 자식이 재정의
- 예: speak() → 강아지는 "멍멍", 고양이는 "야옹"
설계원칙 중요한 이유
- 코드 유지보수성 높임
- 확장성,유연성 확보
- 협업 용이
- 결합도 ↓, 응집도 ↑
2. SOLID 원칙
객체지향 프로그래밍 핵심 원칙. 각 원칙의 앞글자 따서 SOLID
- Single Responsibility Principle (단일 책임 원칙)
- 클래스는 하나의 책임
- Open/Closed Principle (개방-폐쇄 원칙)
- 확장은 열려있고 수정은 닫혀있어야함
- Liskov Substitution Principle (리스코프 치환 원칙)
- 자식은 부모를 대체할 수 있다
- Interface Segregation Principle (인터페이스 분리 원칙)
- 사용하지 않는 기능에 의존하지 않도록 인터페이스는 작게 분리
- Dependency Inversion Principle (의존성 역전 원칙)
- 구체 클래스가 아니라 추상(인터페이스/상위 타입)에 의존
2.1. SRP 단일 책임 원칙
클래스는 단 하나의 책임만 가져야 한다.
SRP 예제와 설명
유저랑 패스워드 검증이 있다고 가정을 한다면 유저정보에 패스워드 검증 책임을 가지면 안된다.
❌ SRP 위반 예시
- 유저 데이터 + 비밀번호 유효성 검사 → 두 책임이 섞임
class User {
String username;
String password;
User(String username, String password) {
this.username = username;
this.password = password;
}
// 유저 데이터 + 비밀번호 유효성 검사 → 두 책임이 섞임
boolean validatePassword() {
return password != null && password.length() >= 8;
}
}
✅ SRP 준수 예시
- 클래스 나눠 책임 분리
// 유저 정보 클래스 (책임: 데이터 보관만)
class User {
String username;
String password;
User(String username, String password) {
this.username = username;
this.password = password;
}
}
// 비밀번호 유효성 검사 클래스 (책임: 검증만)
class PasswordValidator {
boolean isValid(String password) {
return password != null && password.length() >= 8;
}
}
public class UserExam {
public static void main(String[] args) {
PasswordValidator validator = new PasswordValidator();
String rawPassword = "12345678";
if (validator.isValid(rawPassword)) {
User user = new User("hong", rawPassword); // 검증 후 User 생성
System.out.println("User 생성 완료: " + user.username);
} else {
System.out.println("비밀번호 규칙 위반");
}
}
}
2.2. OCP 개방-폐쇄 원칙
확장은 열려있고 수정은 닫혀있어야 한다.
OCP 예제와 설명
결제기능이 있으면 결제기능에 대해 결제수단이 추가될때마다 수정보단 확장으로 되게끔 구현해야 한다.
OCP를 지키면 새로운 기능(결제수단) 추가 시 기존 코드를 수정하지 않고 클래스 확장만으로 해결할 수 있다.
❌ OCP 위반 예시
- 새로운 결제수단이 생길 때마다 PaymentService의 if문 추가 수정해야 함 → OCP 위반
class PaymentService {
public void pay(String method, int amount) {
if (method.equals("CARD")) {
System.out.println("신용카드로 " + amount + "원 결제");
} else if (method.equals("CASH")) {
System.out.println("현금으로 " + amount + "원 결제");
}
// 나중에 PayPal, KakaoPay 추가하려면?
// → else if 계속 늘려야 함
}
}
public class PaymentExam {
public static void main(String[] args) {
PaymentService service = new PaymentService();
service.pay("CARD", 10000);
service.pay("CASH", 5000);
}
}
✅ OCP 준수 예시
- 인터페이스로 나누어 확장
// 공통 결제 인터페이스
interface Payment {
void pay(int amount);
}
// 카드 결제
class CardPayment implements Payment {
public void pay(int amount) {
System.out.println("신용카드로 " + amount + "원 결제");
}
}
// 현금 결제
class CashPayment implements Payment {
public void pay(int amount) {
System.out.println("현금으로 " + amount + "원 결제");
}
}
// 결제 서비스 (Payment 인터페이스에만 의존)
class PaymentService {
public void process(Payment payment, int amount) {
payment.pay(amount);
}
}
public class PaymentExam {
public static void main(String[] args) {
PaymentService service = new PaymentService();
service.process(new CardPayment(), 10000);
service.process(new CashPayment(), 5000);
// 새로운 결제수단 추가? → 클래스만 만들면 됨
service.process(new PaypalPayment(), 20000);
}
}
// 결제 수단 확장시 ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
class PaypalPayment implements Payment {
public void pay(int amount) {
System.out.println("PayPal로 " + amount + "원 결제");
}
}
2.3. LSP 리스코프 치환 원칙
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
LSP 예제와 설명
도형이 있다고 할 때 부모 타입인 Rectangle 자리에 자식 타입인 Square를 넣어도 동일하게 동작해야 한다.
❌ LSP 위반 예시
- 부모 타입(Rectangle)을 쓰는 입장에서는 setWidth와 setHeight가 독립적으로 동작한다고 믿는데
Square는 그 계약을 깨뜨림 → LSP 위반
// 부모: 사각형
class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
// 자식: 정사각형
class Square extends Rectangle {
@Override
public void setWidth(int w) {
this.width = w;
this.height = w; // 가로/세로를 동시에 바꿈
}
@Override
public void setHeight(int h) {
this.width = h;
this.height = h; // 가로/세로를 동시에 바꿈
}
}
public class ShapeExam {
public static void main(String[] args) {
Rectangle rect = new Square(); // 부모 자리에 자식 대입
rect.setWidth(5);
rect.setHeight(10);
// 예상: 면적 = 5 * 10 = 50
// 실제: 면적 = 10 * 10 = 100 (잘못됨)
System.out.println("면적: " + rect.area());
}
}
✅ LSP 준수 예시 (Shape 인터페이스로 분리)
// 부모: 도형 인터페이스
interface Shape {
int area();
}
// 사각형
class Rectangle implements Shape {
private final int width;
private final int height;
Rectangle(int w, int h) { this.width = w; this.height = h; }
public int area() { return width * height; }
}
// 정사각형
class Square implements Shape {
private final int side;
Square(int side) { this.side = side; }
public int area() { return side * side; }
}
public class ShapeExam {
public static void main(String[] args) {
Shape rect = new Rectangle(5, 10);
Shape square = new Square(5);
System.out.println("사각형 면적: " + rect.area()); // 50
System.out.println("정사각형 면적: " + square.area()); // 25
}
}
2.4. ISP 인터페이스 분리 원칙
사용하지 않는 기능에 의존하지 않도록 인터페이스는 작게 분리해야 한다.
ISP 예제와 설명
도형에서 Shape 인터페이스에 너무 많은 기능을 넣으면 특정 도형이 필요하지 않은 기능까지 강제로 구현해야 된다.
ISP를 지키려면 인터페이스를 역할별로 분리해서 각 도형이 필요한 기능만 구현하게 해야 한다.
❌ ISP 위반 예시
- Shape 인터페이스가 setWidth, setHeight, setSide 모두 포함
- Rectangle은 setSide를, Square는 setWidth/setHeight를 억지로 구현해야 함
interface Shape {
void setWidth(int w);
void setHeight(int h);
void setSide(int side);
int area();
}
class Rectangle implements Shape {
private int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public void setSide(int side) {
throw new UnsupportedOperationException("사각형은 setSide 필요 없음");
}
public int area() { return width * height; }
}
class Square implements Shape {
private int side;
public void setWidth(int w) {
throw new UnsupportedOperationException("정사각형은 setWidth 필요 없음");
}
public void setHeight(int h) {
throw new UnsupportedOperationException("정사각형은 setHeight 필요 없음");
}
public void setSide(int side) { this.side = side; }
public int area() { return side * side; }
}
✅ ISP 준수 예시
// 공통 도형 인터페이스 (면적만 공통)
interface Shape {
int area();
}
// 사각형 전용 인터페이스
interface RectangleShape extends Shape {
void setWidth(int w);
void setHeight(int h);
}
// 정사각형 전용 인터페이스
interface SquareShape extends Shape {
void setSide(int side);
}
// 사각형 구현
class Rectangle implements RectangleShape {
private int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}
// 정사각형 구현
class Square implements SquareShape {
private int side;
public void setSide(int side) { this.side = side; }
public int area() { return side * side; }
}
2.5. DIP 의존성 역전 원칙
상위 모듈은 하위 모듈의 구현체가 아닌 추상화에 의존해야 한다.
DIP 예제와 설명
STV, LTV와 같은 구현체에 직접 의존하면 특정 브랜드 TV를 바꿀 때마다 소스를 수정해야 한다.
DIP를 지킬 때, 상위 TV라는 추상화(인터페이스)에 의존하고,
실제 구현체는 외부에서 주입받아 사용하여 교체와 확장이 쉽게 할 수 있다.
❌ DIP 위반 예시
- 메인에서 STV라는 구현체를 직접 생성(new)하고 사용
- 나중에 LG TV로 바꾸려면 상위 모듈 코드를 직접 수정해야 함
// 인터페이스 없이 구현체에 직접 의존하는 나쁜 예
class STV {
private boolean power;
private int volume = 50;
public void powerOn() { power = true; System.out.println("Samsung TV 켜짐"); }
public void powerOff() { power = false; System.out.println("Samsung TV 꺼짐"); }
public void volumeUp() { System.out.println("볼륨: " + (++volume)); }
public void volumeDown(){ System.out.println("볼륨: " + (--volume)); }
}
public class TVExam {
public static void main(String[] args) {
// 상위 모듈이 구현체에 직접 의존 (new STV)
STV tv = new STV();
tv.powerOn(); // 전원 켜기
tv.volumeUp(); // 볼륨 올리기
tv.volumeDown(); // 볼륨 내리기
tv.powerOff(); // 전원 끄기
// 나중에 LG로 바꾸고 싶다면?
// STV tv = new STV(); → LTV tv = new LTV(); 로 소스 수정 필수
}
}
✅ DIP 준수 예시
- 메인에서 TV 인터페이스에만 의존
- 실제 구현체(STV, LTV)는 외부에서 주입받음 → 교체와 확장이 용이
- 팩토리패턴으로 하여금 메인 클래스 수정 없이 가능
// 메인에서 의존할 인터페이스
interface TV {
void powerOn(); // 전원 켜기
void powerOff(); // 전원 끄기
void volumeUp(); // 볼륨 올리기
void volumeDown();// 볼륨 내리기
}
// 구현체 1: samsung TV
class STV implements TV {
private boolean power;
private int volume = 50;
@Override public void powerOn() { power = true; System.out.println("samsung TV 켜짐"); }
@Override public void powerOff() { power = false; System.out.println("samsung TV 꺼짐"); }
@Override public void volumeUp() { System.out.println("볼륨: " + (++volume)); }
@Override public void volumeDown() { System.out.println("볼륨: " + (--volume)); }
}
// 구현체 2: LG TV
class LTV implements TV {
private boolean power;
private int volume = 50;
@Override public void powerOn() { power = true; System.out.println("LG TV 켜짐"); }
@Override public void powerOff() { power = false; System.out.println("LG TV 꺼짐"); }
@Override public void volumeUp() { System.out.println("볼륨: " + (++volume)); }
@Override public void volumeDown() { System.out.println("볼륨: " + (--volume)); }
}
// [선택사항] 팩토리 패턴으로 고도화 → 브랜드명에 따른 구현체 생성 분리
class TVFactory {
public static TV obj(String brand) {
if (brand.equals("samsung")) return new LTV();
if (brand.equals("LG")) return new STV();
throw new IllegalArgumentException("지원하지 않는 브랜드: " + brand);
}
}
public class TVExam {
public static void main(String[] args) {
// 인터페이스 의존 / 구현체 외부주입됨
Tv tv0 = new STV();
tv0.powerOn();
tv0.volumeUp();
tv0.volumeDown();
tv0.powerOff();
// 팩토리패턴
String brand = "LG"; // 전달 받은 값 예시
TV tv1 = TVFactory.create(brand);
tv1.powerOn();
tv1.volumeUp();
tv1.volumeDown();
tv1.powerOff();
}
}
etc.
Q1. SOLID 원칙 무조건 적용 ?
A1: 프로젝트 규모와 복잡도에 따라 적절히 적용하는 것이 중요함
Q2. ISP 원칙으로 인터페이스 너무 많이 만들면 복잡하지 않나 ?
A2: 목적에 맞게 인터페이스 구성하여 적절한 크기로 분리하여 사용하는 것이 중요
Q3. DIP와 의존성 주입(DI) 같은 개념 ?
A3: DIP는 설계원칙이고 DI는 기법 중 하나
- DIP : 무엇을 해야 하는지
- DI : 어떻게 하는지