단일 책임 원칙 SRP(Single Resonsibility Priciple)
- 하나의 클래스는 하나의 책임만 가져야 한다.
- 클래스는 한 가지 기능에 집중해야 하며, 그 외의 기능을 담당하지 않아야 한다.
- 예시
- User 클래스는 사용자 정보 관리, 로그인 및 데이터베이스 저장 책임을 동시에 가지고 있음
//잘못된 예시
public class User {
private String name; // 사용자 정보
public void login() { // 로그인 기능 }
public void saveUser() { // 데이터베이스 저장 기능 }
}
// SRP 적용
public class User { // 사용자 정보 관리 }
public class AuthService {
public void login(User user){
//로그인 기능
}
public class UserRepository {
public void saveUser(User user) {
// 데이터베이스 저장
}
}
개방 폐쇄 원칙 OCP(Open Closed Priciple)
- 소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
- 새로운 기능을 추가할 때 기존 코드를 수정하지 않고, 확장할 수 있도록 설계해야 한다.
- 예시
- 새로운 도형이 추가될 때마다 AreaCalculator 클래스를 수정해야 한다.
public class Shape {
public String type;
}
public class AreaCalculator {
public double calculate(Shape shape) {
if (shape.type.equals("circle")) {
return /* 원의 넓이 계산 */;
} else if (shape.type.equals("square")) {
return /* 사각형의 넓이 계산 */;
}
}
}
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
public double calculateArea() { return /* 원의 넓이 계산 */; }
}
public class Square implements Shape {
public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}
public class AreaCalculator {
public double calculate(Shape shape) {
return shape.calculateArea();
}
}
- 새로운 도형이 추가되더라도 shape 인터페이스만 구현하면 된다.
- AreaCalculator는 수정할 필요가 없다
- 다형성을 활용하여 해결한다.
- 인터페이스를 implements 하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구한현다.
- 역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리한다.
- 문제점
// Circle을 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
Circle circle = new Circle();
areaCalculator.calculate(circle);
}
}
// Square를 계산하는 경우
public class Main {
public static void main(String[]) {
AreaCalculator areaCalculator = new AreaCalculator();
// Circle circle = new Circle();
Square square = new Square();
areaCalculator.calculate(square);
}
- 구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언측의 코드를 변경해야 한다.
- 객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다.
- 무엇인가 = Spring Container 역할
리스코프 치환 원칙 LSP(Liskov Substitution Principle)
- 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.
- 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
- 예시
- ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없다.
//LSP 위반
class Car {
public void accelerate() {
System.out.println("자동차가 휘발유로 가속합니다.");
}
}
class ElectricCar extends Car {
@Override
public void accelerate() {
throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.accelerate(); // "자동차가 가속합니다."
Car electricCar = new ElectricCar();
electricCar.accelerate(); // UnsupportedOperationException 발생
}
}
// 리스코프 치환 원칙 적용
// 가속 기능(역할)을 인터페이스로 분리
interface Acceleratable {
void accelerate();
}
class Car implements Acceleratable {
@Override
public void accelerate() {
System.out.println("내연기관 자동차가 가속합니다.");
}
}
class ElectricCar implements Acceleratable {
@Override
public void accelerate() {
System.out.println("전기차가 배터리로 가속합니다.");
}
}
public class Main {
public static void main(String[] args) {
Acceleratable car = new Car();
car.accelerate(); // "내연기관 자동차가 가속합니다."
Acceleratable electricCar = new ElectricCar();
electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
}
}
- 인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
인터페이스 분리 원칙 ISP(Interface Segregation Principle)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
- 즉, 하나의 큰 인터페이스 보다는 여러 개의 작은 인터페이스로 분리해야 한다.
- 예시
- Dog 클래스는 fly() 메서드를 사용하지 않지만 구현해야 한다.
// 잘못된 예시
public interface Animal {
void fly();
void run();
void swim();
}
public class Dog implements Animal {
public void fly() { /* 사용하지 않음 */ }
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
// 인터페이스 분리 원칙 적용
public interface Runnable {
void run();
}
public interface Swimmable {
void swim();
}
public class Dog implements Runnable, Swimmable { // 인터페이스는 다중 구현 허용
public void run() { /* 달리기 */ }
public void swim() { /* 수영 */ }
}
- 이로써 인터페이스가 명확해졌다.
의존관계 역전 원칙 DIP(Dependency Inversion Principle)
- 구체적인 클래스에 의존하지 말고, 인터페이스나 추상 클래스에 의존하도록 설계해야 한다.
- 예시
- NotificationService는 EmailNotifier 클래스를 의존한다.
// Email 알림 클래스
class EmailNotifier {
public void sendEmail(String message) {
System.out.println("Email 알림: " + message);
}
}
// 알림 시스템
class NotificationService {
private EmailNotifier emailNotifier;
public NotificationService() {
// 구체적인 클래스인 EmailNotifier에 의존
this.emailNotifier = new EmailNotifier();
}
public void sendNotification(String message) {
emailNotifier.sendEmail(message);
}
}
public class Main {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.sendNotification("안녕하세요! 이메일 알림입니다.");
}
}
- email 알림이 아닌 SMS 알림과 같은 기능이 추가되면 NotificationService는 수정되어야 한다. DIP 위반
// 알림 인터페이스(추상화)
interface Notifier {
void send(String message);
}
// Email 알림 클래스
class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("Email 알림: " + message);
}
}
// SMS 알림 클래스
class SMSNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS 알림: " + message);
}
}
// 알림 서비스 (높은 수준 모듈)
class NotificationService {
// 추상화된 인터페이스에 의존
private Notifier notifier;
// 의존성 주입 (생성자를 통해 주입)
public NotificationService(Notifier notifier) {
this.notifier = notifier;
}
public void sendNotification(String message) {
// notifier가 어떤 구현체인지 상관하지 않음
notifier.send(message);
}
}
public class Main {
public static void main(String[] args) {
// Email 알림을 사용
Notifier emailNotifier = new EmailNotifier();
NotificationService emailService = new NotificationService(emailNotifier);
emailService.sendNotification("안녕하세요! 이메일 알림입니다.");
// SMS 알림을 사용
Notifier smsNotifier = new SMSNotifier();
NotificationService smsService = new NotificationService(smsNotifier);
smsService.sendNotification("안녕하세요! SMS 알림입니다.");
}
}
- 추상화 된 Notifier 인터페이스에만 의존한다
- 새로운 알림 방식이 추가되어도 NotificationService는 변경되지 않아도 된다.
- 필요한 Notifier 객체를 외부에서 주입받는다.
- NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도되므로, 의존성이 약해진다.
- 모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
- 서로의 변경 사항에 독립적이어서 변경에 유리하다.
객체지향의 핵심은 다형성에 있다. 하지만 다형성 만으로는 OCP, DIP를 지킬 수 없다.
OCP와 DIP를 보면 결국에는 우리가 원하는 것을 사용하기 위해서는 Main 클래스의 main 메서드 안에서 new 키워드를 통해 인스턴스화를 시켰다. 그래서 기존 코드에 계속해서 변경이 생길 수 밖에 없다.
그래서 이걸 Spring에서는 IOC와 DI를 통해 이 문제를 해결해준다.
Springdms OCP, DIP 원칙을 지킬 수 있도록 도와주고, 구성 요소를 손쉽게 교체하고 결합할 수 있도록 만들어준다.
그래서 우리들은 이상적으로는 모든 설계를 인터페이스로 만들어야 코드가 유연하게 변경이 가능해진다.
하지만 실무에서는 추상화 과정에서 비용이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 하면 된다!
'Back-End > Spring' 카테고리의 다른 글
| IOC / DI (0) | 2024.12.06 |
|---|---|
| Spring Container와 Spring Bean (0) | 2024.12.06 |
| Spring DI(의존성 주입)에 대해서 (1) | 2024.12.01 |
| Spring Annotation (0) | 2024.12.01 |
| 스프링으로 넘어 가기 전에.. (0) | 2024.11.20 |