1. SOLID 원칙
1) SRP (Single Reponsibility Principle) : 단일 책임 원칙
→ 한 클래스는 단 하나의 책임을 가져야 하고, 클래스가 변경되어야 하는 이유는 단 하나의 이유여야 한다.
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public boolean isValid() {
// 유효성 검사
return true;
}
public void save() {
// 데이터 저장
}
}
위 클래스는 사용자 정보를 나타내는 클래스이고, 유효성 검사와 데이터 저장은 사용자 정보와 직접적인 연관이 없는 작업이므로 단일 책임 원칙에 어긋난다. → 클래스를 더 작은 단위로 분리하여 단일 책임을 부여하는 것이 바람직하다.
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
public class UserValidator {
public boolean isValid(User user) {
// 유효성 검사
return true;
}
}
public class UserDAO {
public void save(User user) {
// 데이터 저장
}
}
위와 같이 클래스를 분리하여 User 클래스는 사용자 정보만 관리하고, 유효성 검사와 데이터 저장은 각각 다른 클래스에서 담당한다.
→ 코드의 유지보수성이 증가하고, 다른 기능을 추가하거나 변경할 때 영향을 최소화할 수 있다.
2) OCP (Open-Closed Principle) : 개방-폐쇄 원칙
→ 기존 코드를 변경하지 않으면서 기능을 확장할 수 있도록 설계해야 한다.
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double calculateArea() {
return width * height;
}
}
public class AreaCalculator {
public double calculateArea(Shape[] shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.calculateArea();
}
return totalArea;
}
}
shape 인터페이스는 다양한 도형의 면적을 계산하기 위한 공통 기능을 정의하고, Circle과 Rectangle 클래스는 각각 원과 사각형의 면적을 계산하는 구체적인 기능을 구현한다.
AreaCalculator 클래스는 입력받은 여러 도형의 면적을 모두 더해 총 면적을 계산하는 역할을 하고, 이때 shape 인터페이스를 구현한 어떤 도형 클래스도 입력받을 수 있다. 이로 인해 새로운 도형 클래스가 추가되더라도 AreaCalculator 클래스는 수정할 필요 없이 기존의 동작을 그대로 유지할 수 있다.
3) LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
→ 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 자식의 일은 부모의 일보다 작거나 같아야 한다.
즉, 자식클래스가 부모클래스의 인스턴스 대신 사용될 때 언제나 정상적으로 작동해야 한다.
public class PrintPositiveNum {
private int num;
public PrintPositiveNum(int num){
this.num = num;
}
public getNum(){
if(this.num <= 0){
throw new RuntimeException("0 이하는 출력 불가능!!!");
}
return this.num;
}
}
public class PrintNum extends PrintPositiveNum {
@Override
public getNum(){
return this.num;
}
}
public class Client{
public static void main(String[] args){
PrintPositiveNum obj = new PrintPositiveNum(1);
//PrintPositiveNum obj = new PrintNum(-1); // 이 경우 위반
obj.getNum();
}
}
위 코드에서 PrintPositiveNum은 부모클래스로 양수만 화면에 출력 가능, 자식클래스인 PrintNum은 모든 범위의 숫자 출력 가능
Client 클래스에서 양수를 주는 경우 어떤 타입이 들어와도 실행가능하지만, 음수나 0의 경우 클라이언트측 코드를 수정해야만 함
부모가 수행가능한 범위 내에서만 오버라이딩을 해야 클라이언트측 코드를 고칠 필요가 없어진다는 것이 리스코프 치환 원칙의 핵심
4) ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
→ 인터페이스는 클라이언트에 특화되어야 하고, 클라이언트가 사용하지 않는 메서드는 포함하지 않아야 한다.
public interface Shape {
double calculateArea();
double calculateVolume();
}
public class Rectangle implements Shape {
private double width;
private double height;
public double calculateArea() {
return width * height;
}
public double calculateVolume() {
throw new UnsupportedOperationException();
}
}
public class Cube implements Shape {
private double width;
private double height;
private double depth;
public double calculateArea() {
return 2 * (width * height + width * depth + height * depth);
}
public double calculateVolume() {
return width * height * depth;
}
}
Shape 인터페이스는 도형의 면적과 부피를 계산하는 두 가지 메서드를 정의하는데, Rectangle 클래스는 면적만 가능하고 부피를 계산할 수 없기 때문에 ISP를 위반한다. 즉, 클라이언트는 Shape 인터페이스를 구현한 모든 클래스에서 부피 계산 메서드를 사용해야 하기 때문에 불필요한 의존성이 발생된다.
public interface Area {
double calculateArea();
}
public interface Volume {
double calculateVolume();
}
public class Rectangle implements Area {
private double width;
private double height;
public double calculateArea() {
return width * height;
}
}
public class Cube implements Area, Volume {
private double width;
private double height;
private double depth;
public double calculateArea() {
return 2 * (width * height + width * depth + height * depth);
}
public double calculateVolume() {
return width * height * depth;
}
}
위와 같이 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 분리해야 한다.
5) DIP (Dependency Inversion Principle) : 의존 역전 원칙
→ 상위 수준 모듈은 하위 수준 모듈에 의존하지 않아야 하고, 추상화는 구체적인 사항에 의존하지 않아야 한다.
① 고차원 모듈은 저차원 모듈에 의존해서는 안 된다.
② 추상화는 세부사항에 의존해서는 안된다.
public class RedLight {
public void turnOn() {
System.out.println("Red Light turned on");
}
}
public class Switch {
private RedLight light;
public Switch() {
this.light = new RedLight();
}
public void flip() {
if (light != null) {
light.turnOn();
}
}
}
위 코드에서 Switch 클래스는 RedLight 클래스를 직접 생성하고 사용하므로 Switch 클래스가 Redlight 클래스에 의존하게 된다.
만약 RedLight 클래스를 BlueLight 클래스로 변경한다면 Switch 클래스도 변경해야 하는 문제가 발생한다.
public interface Light {
void turnOn();
}
public class RedLight implements Light {
@Override
public void turnOn() {
System.out.println("Red Light turned on");
}
}
public class Switch {
private Light light;
public Switch(Light light) {
this.light= light;
}
public void flip() {
if (light!= null) {
light.turnOn();
}
}
}
위 코드에서 Switch 클래스는 Light 인터페이스를 통해 RedLight 클래스와 의존 관계를 맺는다. 이렇게 함으로써 RedLight 클래스에 변경이 생긴다고 해도 Switch 클래스는 영향을 받지 않는다.