S : Single Responsibility Principle(단일 책임 원칙)
: 하나의 클래스는 하나의 책임만 가져야 하며, 클래스가 변경되는 이유는 단 하나뿐이어야 한다.
-> 코드를 명확하게 유지하고, 기능 변경 시 영향을 최소화
ex) ReportGenerator 클래스가 데이터를 분석하고 파일로 저장까지 모두 처리한다 DataAnalyzer와 FileSaver 클래스로 분리.
// 잘못된 설계: 하나의 클래스가 여러 책임을 가짐
class ReportGenerator {
public void analyzeData() {
// 데이터 분석 로직
}
public void saveToFile() {
// 파일 저장 로직
}
}
// 개선된 설계: 각 클래스가 단일 책임만 가짐
class DataAnalyzer {
public void analyzeData() {
// 데이터 분석 로직
}
}
class FileSaver {
public void saveToFile(String data) {
// 파일 저장 로직
}
}
// 사용 예시
DataAnalyzer analyzer = new DataAnalyzer();
String analyzedData = "analyzed data"; // 예제 데이터
FileSaver saver = new FileSaver();
saver.saveToFile(analyzedData);
O : Open/Closed Principle(개방/폐쇄 원칙)
: 소프트웨어는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
-> 기존 코드를 변경하지 않고도 기능을 확장할 수 있게 설계
ex) 새로운 할인 정책을 추가할 때 기존 DiscountPolicy 인터페이스를 구현하는 새로운 클래스를 작성하면 됨. 기존 코드 수정 없이 확장 가능.
// 기존 코드: DiscountPolicy 인터페이스
interface DiscountPolicy {
double calculateDiscount(double price);
}
// 기본 할인 정책
class PercentageDiscount implements DiscountPolicy {
@Override
public double calculateDiscount(double price) {
return price * 0.1; // 10% 할인
}
}
// 새로운 할인 정책 추가 (기존 코드 수정 불필요)
class FixedAmountDiscount implements DiscountPolicy {
@Override
public double calculateDiscount(double price) {
return 5000; // 5,000원 할인
}
}
// 사용 예시
class Order {
private DiscountPolicy discountPolicy;
public Order(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public double calculateFinalPrice(double price) {
return price - discountPolicy.calculateDiscount(price);
}
}
// 실행
DiscountPolicy discountPolicy = new FixedAmountDiscount();
Order order = new Order(discountPolicy);
System.out.println(order.calculateFinalPrice(30000)); // 출력: 25000
L : Liskov Substiution Principle(리스코프 치환 원칙)
: 상위 클래스의 객체를 하위 클래스의 객체로 치환해도 프로그램의 동작이 일관되어야 한다.
-> 다형성을 제대로 활용하고, 예기치 않은 버그를 방지
ex)
Rectangle의 서브클래스 Square가 있다면, setWidth와 setHeight 메서드가 일관성 있게 동작해야 함.
잘못된 설계: Square가 Rectangle의 규칙을 어기는 경우.
// 잘못된 설계: Square가 Rectangle 규칙을 어김
class Rectangle {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 너비 설정 시 높이도 같이 변경
}
@Override
public void setHeight(int height) {
super.setWidth(height); // 높이 설정 시 너비도 같이 변경
super.setHeight(height);
}
}
// 문제 발생: Rectangle로 처리 시 예상치 못한 결과
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // 예상: 50, 실제: 100 (오류)
// 올바른 설계: Square는 별도로 분리
class Square {
private int side;
public void setSide(int side) {
this.side = side;
}
public int getArea() {
return side * side;
}
}
I : Interface Segregation Principle (인터페이스 분리 원칙)
: 특정 클라이언트가 사용하지 않는 메서드가 포함된 인터페이스를 구현하지 않도록 해야 한다.
-> 불필요한 의존성을 줄이고, 인터페이스를 작게 나누어 유연성 증대.
ex) Printer 인터페이스에 scan 기능이 있다면, 단순 프린터 클래스가 이를 구현할 필요가 없도록 Printable과 Scannable로 분리
// 잘못된 설계: 모든 프린터가 scan 메서드를 구현해야 함
interface Printer {
void print(String document);
void scan(String document);
}
// 개선된 설계: 인터페이스를 분리
interface Printable {
void print(String document);
}
interface Scannable {
void scan(String document);
}
// 구현 클래스
class SimplePrinter implements Printable {
@Override
public void print(String document) {
System.out.println("Printing: " + document);
}
}
class AdvancedPrinter implements Printable, Scannable {
@Override
public void print(String document) {
System.out.println("Printing: " + document);
}
@Override
public void scan(String document) {
System.out.println("Scanning: " + document);
}
}
D : Dependency Inversion Principle (의존 역전 원칙)
: 고수준 모듈이 저수준 모듈에 의존해서는 안되며 둘 다 추상화에 의존해야 한다.
-> 구현 세부사항에 의존하지 않고, 추상화를 통해 유연성과 테스트 용이성 증대.
ex) 구체적인 MySQLDatabase 클래스에 의존하지 않고 Database 인터페이스를 사용하여 다양한 데이터베이스 구현을 교체 가능하게 설계
// 잘못된 설계: 고수준 모듈이 저수준 모듈에 직접 의존
class MySQLDatabase {
public void connect() {
System.out.println("Connected to MySQL Database");
}
}
class Application {
private MySQLDatabase database;
public Application() {
database = new MySQLDatabase(); // 구체적인 클래스 의존
}
public void start() {
database.connect();
}
}
// 개선된 설계: 인터페이스 사용
interface Database {
void connect();
}
class MySQLDatabase implements Database {
@Override
public void connect() {
System.out.println("Connected to MySQL Database");
}
}
class PostgreSQLDatabase implements Database {
@Override
public void connect() {
System.out.println("Connected to PostgreSQL Database");
}
}
class Application {
private Database database;
public Application(Database database) {
this.database = database; // 추상화에 의존
}
public void start() {
database.connect();
}
}
// 실행
Database database = new PostgreSQLDatabase();
Application app = new Application(database);
app.start(); // 출력: Connected to PostgreSQL Database
신입 Java/Spring 백엔드 개발자 입장에서 SOLID 원칙을 실습할 수 있는 방법을 아래와 같이 제안드립니다. 실습은 직접 코드를 작성하고, 작동 원리를 이해하며 개선하는 과정을 포함합니다.
실습 아이디어:
PostService가 글 생성, 수정, 삭제, 데이터 검증, 파일 저장 등의 모든 역할을 하도록 작성.PostValidationService, PostFileService 등으로 분리.class PostService {
public void createPost(String title, String content) {
// 데이터 검증
validatePost(title, content);
// 파일 저장
saveToFile(content);
System.out.println("Post created: " + title);
}
private void validatePost(String title, String content) {
if (title == null || title.isEmpty()) {
throw new IllegalArgumentException("Title is required");
}
if (content == null || content.isEmpty()) {
throw new IllegalArgumentException("Content is required");
}
}
private void saveToFile(String content) {
// 파일 저장 로직
System.out.println("File saved: " + content);
}
}
분리 후 개선:
PostValidationService와 PostFileService 클래스를 생성해 분리.실습 아이디어:
DiscountPolicy 인터페이스를 작성.구체적 실습:
@Component와 @Qualifier를 사용하여 새로운 할인 정책을 주입.실습 아이디어:
Rectangle과 Square를 구현.Rectangle로 동작하도록 설계한 코드에서 Square가 의도대로 동작하지 않는 사례를 실습.Square를 별도의 클래스로 분리.구체적 실습:
실습 아이디어:
DocumentPrinter, DocumentScanner 인터페이스를 분리.구체적 실습:
@Component로 등록하고, 필요한 인터페이스만 DI로 주입.SimplePrinter 클래스는 Printable만 구현하고, AdvancedPrinter는 둘 다 구현.실습 아이디어:
Database 인터페이스를 작성하여 MySQL과 PostgreSQL 연결 구현체를 만들어 사용.MockDatabase를 만들어 의존성 역전을 실험.interface Database {
void connect();
}
@Component
class MySQLDatabase implements Database {
public void connect() {
System.out.println("Connected to MySQL");
}
}
@Component
class PostgreSQLDatabase implements Database {
public void connect() {
System.out.println("Connected to PostgreSQL");
}
}
@Service
class Application {
private final Database database;
@Autowired
public Application(@Qualifier("mySQLDatabase") Database database) {
this.database = database;
}
public void run() {
database.connect();
}
}
Spring 프로젝트 초기화:
테스트 기반 실습:
GitHub에 프로젝트 관리:
Spring 컨테이너 활용:
이러한 실습 과정을 통해 실무에 바로 적용 가능한 객체지향 설계와 Spring 활용 능력을 키울 수 있습니다. 각 실습은 작은 프로젝트로도 진행 가능하며, 성장에 도움이 됩니다.