📌 면접 답변
SOLID 원칙은 객체지향 설계의 핵심인 의존성 관리를 위한 원칙이다.
객체지향 프로그래밍을 하면서 지켜야 할 5대 원칙으로, 변경에 용이하고, 유지보수와 확장에 도움이 된다.
SRP (SIngle Responsibility Priciple, 단일 책임 원칙)
하나의 클래스가 여러 가지 기능을 담당하면 안 되고, 하나의 역할(책임)만 수행해야 한다.
SRP를 잘 지키면 변경이 필요할 때 수정할 대상이 명확해진다.
SRP 위반 예시
class Report {
public String generate() {
return "Report Content";
}
public void print() {
System.out.println("Printing Report...");
}
public void saveToFile() {
System.out.println("Saving Report to File...");
}
}
- Report 클래스가 generate, print, saveToFile 모두 처리
- 만약 saveToFile의 방식이 변경되면 클래스 전체를 수정 (문제점)
올바른 SRP 적용 예시
// 1. Report 생성 역할
class Report {
public String generate() {
return "Report Content";
}
}
// 2. 출력 역할 분리
class ReportPrinter {
public void print(Report report) {
System.out.println("Printing: " + report.generate());
}
}
// 3. 파일 저장 역할 분리
class ReportSaver {
public void saveToFile(Report report) {
System.out.println("Saving to file: " + report.generate());
}
}
- 각각의 클래스가 하나의 책임만 갖는다.
- 수정이 필요할 때 관련 클래스만 변경하는 장점
- 가독성 증가, 유지보수 용이, 유닛 테스트 가능 (장점)
OCP (Open-Closed Priciple, 개방 폐쇄 원칙)
- 확장에는 열려있고, 변경에는 닫혀 있어야 함
- 확장은 새로운 타입을 추가함으로써 새로운 기능 추가를 의미
- 폐쇄는 확장이 일어날 때 상위 레벨의 모듈이 영향을 받지 않아야 함을 의미
- 이를 통해 모듈의 행동을 쉽게 변경 가능
OCP 위반 예시
class DiscountService {
public double calculateDiscount(String type, double price) {
if (type.equals("STUDENT")) {
return price * 0.9; // 학생 할인 (10%)
} else if (type.equals("VIP")) {
return price * 0.8; // VIP 할인 (20%)
} else {
return price; // 할인 없음
}
}
}
- 할인 정책이 변경 될 때마다 calculateDiscount()를 수정해야 함 (문제점)
올바른 OCP 적용 예시
// 1. 할인 정책 인터페이스
interface DiscountPolicy {
double applyDiscount(double price);
}
// 2. 학생 할인 (10%)
class StudentDiscount implements DiscountPolicy {
public double applyDiscount(double price) {
return price * 0.9;
}
}
// 3. VIP 할인 (20%)
class VIPDiscount implements DiscountPolicy {
public double applyDiscount(double price) {
return price * 0.8;
}
}
// 4. 할인 서비스 (OCP 적용)
class DiscountService {
private final DiscountPolicy discountPolicy;
public DiscountService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public double calculateDiscount(double price) {
return discountPolicy.applyDiscount(price);
}
}
- 이렇게 할 경우 새로운 할인 정책이 필요해도 기존 코드를 수정하지 않고 새로운 클래스를 추가하면 된다.
// 4. 블랙프라이데이 할인 (50%)
class BlackFridayDiscount implements DiscountPolicy {
public double applyDiscount(double price) {
return price * 0.5;
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
DiscountService studentDiscountService = new DiscountService(new StudentDiscount());
System.out.println("학생 할인 가격: " + studentDiscountService.calculateDiscount(100));
DiscountService vipDiscountService = new DiscountService(new VIPDiscount());
System.out.println("VIP 할인 가격: " + vipDiscountService.calculateDiscount(100));
DiscountService blackFridayDiscountService = new DiscountService(new BlackFridayDiscount());
System.out.println("블랙프라이데이 할인 가격: " + blackFridayDiscountService.calculateDiscount(100));
}
}
- 새로운 기능이 추가되었는데, 기존 코드 수정 없이 확장이 가능해졌다.
- OCP가 본질적으로 얘기하는 것은 추상화로, 이 코드에서 추상화는 DiscountPolicy 이다.
- 추상화를 통해 변하는 것들을 숨기고, 변하지 않는 것들에 의존한다.
- 변하지 않는것은 추상화(인터페이스)
LSP (Liskov Substitution Priciple, 리스코브 치환 원칙)
- 하위 타입은 언제나 상위 타입으로 교체할 수 있어야 함
- 즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다.
LSP 위반 예시
// 1. 부모 클래스: 직사각형
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// 2. 자식 클래스: 정사각형 (직사각형을 상속받음)
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 정사각형이므로 width = height 유지
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height; // 정사각형이므로 width = height 유지
}
}
// 3. LSP 위반이 발생하는 테스트 코드
public class Main {
public static void main(String[] args) {
Rectangle rect = new Square(); // 부모 클래스로 대체
rect.setWidth(4);
rect.setHeight(5);
System.out.println("Expected area: 4 * 5 = 20");
System.out.println("Actual area: " + rect.getArea()); // 예상과 다름 (LSP 위반)
}
}
- 자식 클래스인 Square가 부모 클래스인 Rectangle을 대체 할 수 없음 (문제점)
- Rectangle을 기대하는 코드에 Square를 넣으면 예상과 다른 동작 발생
올바른 LSP 적용 예시
// 1. 공통 인터페이스 생성
interface Shape {
int getArea();
}
// 2. 직사각형 클래스 (Shape 구현)
class Rectangle implements Shape {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
// 3. 정사각형 클래스 (Shape 구현)
class Square implements Shape {
private int side;
public void setSide(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
// 4. 테스트 코드 (LSP 만족)
public class Main {
public static void main(String[] args) {
Shape rect = new Rectangle();
((Rectangle) rect).setWidth(4);
((Rectangle) rect).setHeight(5);
Shape square = new Square();
((Square) square).setSide(4);
System.out.println("Rectangle area: " + rect.getArea()); // 4 * 5 = 20
System.out.println("Square area: " + square.getArea()); // 4 * 4 = 16
}
}
- 이런 경우 Rectangle과 Square를 서로 독립적인 클래스로 나눈다.
- 그리고 공통 인터페이스 Shape을 만들어 각 클래스가 자신의 규칙대로 구현하도록 설계
ISP (Interface Segregation Priciple, 인터페이스 분리 원칙)
- 인터페이스는 클라이언트가 필요로 하는 메서드만 제공해야 한다.
- 불필요한 메드가 포함된 거대한 인터페이스를 만들지 말고, 작은 인터페이스로 나눠야 한다.
ICP 위반 예시
// 1. 너무 큰 인터페이스 (SRP 위반)
interface Worker {
void work(); // 작업 수행
void cookFood(); // 요리 기능
}
// 2. 개발자 클래스
class Developer implements Worker {
public void work() {
System.out.println("개발 중...");
}
public void cookFood() {
throw new UnsupportedOperationException("개발자는 요리를 하지 않습니다!");
}
}
// 3. 요리사 클래스
class Chef implements Worker {
public void work() {
System.out.println("요리 중...");
}
public void cookFood() {
System.out.println("음식을 요리 중...");
}
}
- Worker 인터페이스가 모든 작업자를 위한 인터페이스
- 굳이 Developer는 cookFood를 구현할 필요가 없음 (문제점)
올바른 ISP 적용 예시
// 1. 작업 인터페이스 (공통)
interface Workable {
void work();
}
// 2. 요리 인터페이스 (요리 가능한 사람만)
interface Cookable {
void cookFood();
}
// 3. 개발자 클래스 (요리 기능 없음)
class Developer implements Workable {
public void work() {
System.out.println("개발 중...");
}
}
// 4. 요리사 클래스 (요리 기능 포함)
class Chef implements Workable, Cookable {
public void work() {
System.out.println("요리 중...");
}
public void cookFood() {
System.out.println("음식을 요리 중...");
}
}
- Worker 인터페이스를 작은 역할별 인터페이스로 분리
- 인터페이스의 변경이 특정 클래스에 영향을 미치지 않음
- 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공해야함 !
DIP (Dependency Inversion Priciple, 의존성 역전 원칙)
- 상위 수준의 모듈이 하위 수준의 모듈에 의존해서는 안됨
- 즉, 고수준인 클라이언트가 하위 모듈들에 의존하면 안되고 추상화된 인터페이스에 의존해야 함
- 다른 말로 비즈니스와 관련된 부분이 세부 사항에 의존하지 않는 설계 원칙
- 모두 추상화에 의존을 강조해야 한다.
DIP 위반 예시
// 1. MySQLDatabase 클래스 (저수준 모듈)
class MySQLDatabase {
public void connect() {
System.out.println("MySQL 데이터베이스 연결됨");
}
public void save(String data) {
System.out.println("MySQL에 '" + data + "' 저장 완료");
}
}
// 2. UserService 클래스 (고수준 모듈) - 직접 MySQLDatabase에 의존 (DIP 위반)
class UserService {
private MySQLDatabase database;
public UserService() {
this.database = new MySQLDatabase(); // 특정 DB에 강하게 의존
}
public void saveUser(String username) {
database.connect();
database.save(username);
}
}
// 3. 실행 코드
public class Main {
public static void main(String[] args) {
UserService userService = new UserService();
userService.saveUser("Kim");
}
}
- UserService가 MySQLDatabase에 직접 의존하고 있다.
- 다른 DB로 변경할 경우 UserService를 수정해야 하는 DIP 위반이 발생 (문제점)
올바른 DIP 적용 예시
// 1. 추상화 계층 (인터페이스)
interface Database {
void connect();
void save(String data);
}
// 2. MySQL 구현체 (저수준 모듈)
class MySQLDatabase implements Database {
public void connect() {
System.out.println("MySQL 데이터베이스 연결됨");
}
public void save(String data) {
System.out.println("MySQL에 '" + data + "' 저장 완료");
}
}
// 3. PostgreSQL 구현체 (새로운 DB 추가)
class PostgreSQLDatabase implements Database {
public void connect() {
System.out.println("PostgreSQL 데이터베이스 연결됨");
}
public void save(String data) {
System.out.println("PostgreSQL에 '" + data + "' 저장 완료");
}
}
// 4. UserService가 구체적인 DB가 아닌 인터페이스(Database)에 의존 (DIP 적용)
class UserService {
private Database database;
public UserService(Database database) { // 의존성 주입 (Dependency Injection)
this.database = database;
}
public void saveUser(String username) {
database.connect();
database.save(username);
}
}
// 5. 실행 코드
public class Main {
public static void main(String[] args) {
Database mysqlDB = new MySQLDatabase();
UserService userService1 = new UserService(mysqlDB);
userService1.saveUser("Kim");
Database postgresDB = new PostgreSQLDatabase();
UserService userService2 = new UserService(postgresDB);
userService2.saveUser("Lee");
}
}
- 인터페이스를 만들어 UserService가 추상화에 의존하도록 한다.
- 이제 어떤 DB든 쉽게 교체가 가능하다.
📌 내 답변
SOLID 원칙은 객체지향 설계에서 지켜야 할 원칙이다.
S : 객체는 단일 책임을 가진다.
O : 확장은 쉽고, 코드 변경 사항은 작게 해야 한다.
L : 리스코브 원칙
I, D : ... ??
SOLID 원칙에 대해 대충 알고는 있었지만, 막상 대답을 하려하니 잘 나오지 않았다...
SOLID 원칙은 가장 객체 지향의 기본적인 원칙으로, 핵심은 결국 추상화와 다형성이다.
이 원칙을 통해 유연하고 확장가능한 애플리케이션을 개발 할 수 있다.
잘 알아두자 !!
[출처 및 참고 자료]
매일메일 - 기술 면접 질문 구독 서비스
기술 면접 질문을 매일매일 메일로 보내드릴게요!
www.maeil-mail.kr
매일 메일의 면접 질문 정리
https://blog.siner.io/2020/06/18/solid-principles/
[번역] 그림으로 보는 SOLID 원칙
SOLID 원칙과 관련된 좋은 그림예시가 있어서 이를 번역하면서 예제코드를 추가하였습니다. 만약 당신이 객체지향 프로그래밍과 친숙하다면, 당신은 SOLID 원칙에 대해 들어보았을 것 입니다. 이
blog.siner.io
SOLID 원칙 그림 출처
'😀 Jerry > 면접 질문' 카테고리의 다른 글
[1분 면접] 로드 밸런싱이란 ? (0) | 2025.03.13 |
---|---|
[1분 면접] 다중 서버 환경에서 세션 기반 인증 방식 사용의 문제점 (0) | 2025.03.12 |
[1분 면접] DB Replication이란 ? (0) | 2025.03.10 |
[1분 면접] record를 DTO로 사용하는 이유 (0) | 2025.03.07 |
[1분 면접] HTTPS란 무엇인가 ? (0) | 2025.03.06 |