로버트 C. 마틴이 소개한 객체 지향 프로그래밍 및 설계의 다섯가지 원칙을 말한다.
하지만 정작 지키면서 코드 작성하기는 쉽지 않다 ㅎ;;
- 단일 책임 원칙 (Single Responsibility Principle)
- 개방 - 폐쇄 원칙 (Open/Closed Principle)
- 리스코프 치환 원칙 (Liskov Substitution Principle)
- 인터페이스 분리 원칙 (Interface Segregation Principle)
- 의존성 역전 원칙 (Dependency Inversion Principle)
하나씩 코드로 알아보자
단일 책임 원칙 - SRP
클래스는 하나의 책임만을 가져야 한다
class UserHandler {
private String name;
private String email;
public UserHandler(String name, String email) {
this.name = name;
this.email = email;
}
// 책임 1: 사용자 정보 관련
public String getName() {
return name;
}
public void changeEmail(String newEmail) {
this.email = newEmail;
}
// 책임 2: 데이터베이스 관련
public void saveUserToDatabase() {
// 데이터베이스에 사용자를 저장하는 로직
System.out.println("데이터베이스에 " + name + "을(를) 저장합니다.");
}
}
위 UserHandler를 보자. 이 클래스는 사용자의 정보를 관리하며, 동시에 DB에 사용자를 저장하는 로직을 가지고 있다.
이렇게 하나의 클래스가 여러개의 책임을 가지게 되면 하나의 클래스가 너무 비대해질 뿐더러 코드의 수정이 필요할 때 자칫하면 원하는 부분이 아닌 다른 책임을 가진 코드에서 예상치 못한 에러가 발생할 수 있다.
이는 SRP를 위배한다. 이를 사용자의 정보 관리 책임과 DB 관련 책임으로 분리하자.
// 사용자 데이터만 책임지는 클래스
class User {
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() { return name; }
public String getEmail() { return email; }
public void changeEmail(String newEmail) {
this.email = newEmail;
}
}
// 사용자 데이터베이스 작업만 책임지는 클래스
class UserRepository {
public void save(User user) {
// 데이터베이스에 사용자를 저장하는 로직
System.out.println("데이터베이스에 " + user.getName() + "을(를) 저장합니다.");
}
}
이렇게 두개의 책임을 분리했다. 이제 각 클래스는 알아서 놀면 된다. 사용자 데이터에 수정이 필요하면 User 클래스를 수정하면 되고, DB 관련 로직에 수정이 필요하면 UserRepository를 수정하면 끝이다.
개방 폐쇄의 원칙 - OCP
확장에는 열려있어야 하며, 수정에는 닫혀있어야 한다.
기능을 추가할 때는 클래스 확장을 통해 손쉽게 추가하며, 수정은 최소화해야한다.
class PaymentProcessor {
public void process(String type, double amount) {
if ("creditCard".equals(type)) {
System.out.println(amount + "원 신용카드 결제 처리");
} else if ("paypal".equals(type)) {
System.out.println(amount + "원 페이팔 결제 처리");
}
// 새로운 결제 방식(예: kakaoPay)을 추가하려면 여기를 수정해야 함
}
}
결제를 담당하는 class가 위처럼 작성되어있다고 가정하자.
결제 유형을 추가할때마다 이 클래스에는 수정이 있어야 한다.
interface PaymentGateway {
void pay(double amount);
}
class CreditCardPayment implements PaymentGateway {
@Override
public void pay(double amount) {
System.out.println(amount + "원 신용카드 결제 처리");
}
}
class PaypalPayment implements PaymentGateway {
@Override
public void pay(double amount) {
System.out.println(amount + "원 페이팔 결제 처리");
}
}
// 새로운 결제 방식 추가 (기존 코드 수정 없음)
class KakaoPayPayment implements PaymentGateway {
@Override
public void pay(double amount) {
System.out.println(amount + "원 카카오페이 결제 처리");
}
}
class PaymentProcessor {
public void process(PaymentGateway gateway, double amount) {
gateway.pay(amount);
}
}
pay라는 메소드를 가진 PaymentGateway 인터페이스를 생성하고, PaymentProcessor는 이 인터페이스만 사용하면 된다.
그리고 각 결제 방식은 PaymentGateway를 구현하여 자신의 결제 방식을 작성하기만 하면 된다!!
이러면 새로운 결제 방식이 얼마나 추가되든 PaymentGateway만 구현하기만 하면 끝이다. 머 인터페이스가 아니라 추상 클래스여도 같은 얘기인것 같다.
리스코프 치환 원칙 - LSP
서브 타입은 언제든 부모 타입으로 치환될수 있어야 한다.
부모 타입을 상속받은 클래스가 업캐스팅된 상태에서 부모의 메소드를 사용해도 동작이 의도대로 동작해야하는 것을 의미한다.
다형성을 사용하기 위한 원칙으로 보면 되겠다.
class Bird {
public void fly() {
System.out.println("새가 하늘을 납니다.");
}
}
class Penguin extends Bird {
@Override
public void fly() {
// 펭귄은 날 수 없으므로 이 메소드는 문제를 일으킨다.
throw new UnsupportedOperationException("펭귄은 날 수 없습니다!");
}
public void swim() {
System.out.println("펭귄이 수영합니다.");
}
}
// Bird 타입으로 Penguin을 사용하면 fly() 호출 시 예외 발생
Bird bird = new Penguin();
bird.fly(); // LSP 위반으로 인해 예외 발생!
살짝 억지같은 코드이지만, 펭귄은 하늘을 날 수 없다. 따라서 fly를 사용하면 예외가 발생한다고 해두자.
Penguin 클래스를 Bird 클래스로 업캐스팅한 다음, fly를 사용해도 정상적으로 작동해야 하지만, 펭귄은 날 수 없으니까 예외가 발생한다. (물론 예외처리도 개발자의 의도라 정상적 작동이지만, 현실세계에 비유해서 예외가 발생하면 안된다고 하자.)
class Bird {
// 공통적인 행동
}
interface Flyable {
void fly();
}
// 날 수 있는 새는 Flyable 인터페이스를 구현
class Eagle extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("독수리가 하늘을 높이 납니다.");
}
}
class Penguin extends Bird {
public void swim() {
System.out.println("펭귄이 수영합니다.");
}
}
이렇게 작성하면 하늘을 날 수 있는 독수리만 Flyable 인터페이스를 구현하면 되기에 LSP를 위반하지 않는다.
살~짝 억지같은 코드가 맞긴 한데.. 다음 코드를 보면 이해가 훨씬 쉽다.
Collection data = new LinkedList();
data.add(1); // 아주 잘 됨
이렇게 LinkedList가 Collection으로 업캐스팅되어도 본연의 메소드를 잘 수행했다!!
인터페이스 분리 원칙 - ISP
클래스는 자신이 사용하지 않는 메소드에 의존하도록 강요되어서는 안된다.
한마디로, 본인이 사용하지 않는 메소드를 구현하지 말라는 뜻이다.
interface Worker {
void work();
void eat();
}
class HumanWorker implements Worker {
public void work() { System.out.println("사람이 일합니다."); }
public void eat() { System.out.println("사람이 밥을 먹습니다."); }
}
class RobotWorker implements Worker {
public void work() { System.out.println("로봇이 일합니다."); }
// 로봇은 먹을 수 없지만, 인터페이스 때문에 강제로 구현해야 함
public void eat() {
// 아무것도 안 함 또는 예외 발생
}
}
위 코드를 보면, 로봇은 eat()이 필요가 없음에도 인터페이스 때문에 강제로 구현을 해야한다. 이는 당연히 낭비이다.
interface Workable {
void work();
}
interface Eatable {
void eat();
}
// 사람은 일하고 먹을 수 있으므로 두 인터페이스 모두 구현
class HumanWorker implements Workable, Eatable {
public void work() { System.out.println("사람이 일합니다."); }
public void eat() { System.out.println("사람이 밥을 먹습니다."); }
}
// 로봇은 일만 할 수 있으므로 Workable 인터페이스만 구현
class RobotWorker implements Workable {
public void work() { System.out.println("로봇이 일합니다."); }
}
이렇게 Workable, Eatable로 나누면, 로봇은 eat을 구현하지 않아도 된다.
의존성 역전 원칙 - DIP
구현에 의존하지 말고, 추상화에 의존하라.
어떤 클래스에 의존해야 한다면, 그 클래스를 직접 의존하지 말고 그 클래스의 인터페이스를 의존해야 한다는 원칙이다.
class MySQLConnection {
public String connect() {
return "MySQL 데이터베이스 연결";
}
}
// 구현 클래스에 직접 의존
class PasswordReminder {
private MySQLConnection dbConnection;
public PasswordReminder() {
this.dbConnection = new MySQLConnection(); // 직접적인 의존성
}
public void remind() {
System.out.println(dbConnection.connect());
// ...
}
}
위 코드에서 PasswordReminder는 MySQLConnection에 직접 의존하고 있다. 만약 데이터베이스가 MySQL이 아닌 다른 DBMS에 의존해야 한다면, PasswordReminder를 직접 수정해야 한다.
interface DBConnectionInterface {
String connect();
}
// 인터페이스 구현
class MySQLConnection implements DBConnectionInterface {
@Override
public String connect() {
return "MySQL 데이터베이스 연결";
}
}
class PostgreSQLConnection implements DBConnectionInterface {
@Override
public String connect() {
return "PostgreSQL 데이터베이스 연결";
}
}
// 구현 클래스가 아닌 인터페이스에 의존
class PasswordReminder {
private DBConnectionInterface dbConnection;
// 의존성 주입(DI): 외부에서 생성된 객체를 전달받음
public PasswordReminder(DBConnectionInterface dbConnection) {
this.dbConnection = dbConnection;
}
public void remind() {
System.out.println(dbConnection.connect());
// ...
}
}
// 사용 예시
PasswordReminder reminder = new PasswordReminder(new MySQLConnection());
PasswordReminder reminder2 = new PasswordReminder(new PostgreSQLConnection());
MySQL이나 PostreSQL 클래스는 DBConnectionInterface를 구현하고, PasswordReminder는 이 인터페이스를 의존하기만 하면 된다. 이후에는 그저 구현체만 바꿔서 전달해주기만 하면 된다. PasswordReminder 자체의 수정은 하나도 없는 것이다.
위 5개 원칙을 잘 보면, 수정을 쉽게 하기 위해 강박적인 집착을 보이는것 같다. 하긴, 개발하는것보다 유지보수가 더 어렵다고 하니.
누가 그랬는데.. 변하지 않는것은 변화 뿐이라고
'Java' 카테고리의 다른 글
| JVM 튜?닝? (1) | 2025.11.27 |
|---|---|
| 자바 가비지 컬렉터 (GC) (0) | 2025.09.07 |
| JVM (0) | 2025.09.06 |
| 와일드카드<?>가 무엇인가? (0) | 2025.08.23 |
| Supplier는 왜 쓰는걸까? (0) | 2025.08.16 |