Java 객체지향 설계 방법 정리 (SOLID 원칙, 디자인패턴)
객체지향 설계 원칙 (SOLID 원칙)
단일 책임 원칙 (SRP)
클래스는 하나의 책임만 가져야 합니다.
각 클래스는 여러 함수를 가질 수 있지만, 한가지 역할을 수행해야 합니다.
단일 책임 원칙 위반 예시
public class UserService {
public void saveUser(User user) {
// 데이터베이스에 유저 정보 저장 로직
}
public void sendWelcomeEmail(User) user {
// 유저에게 환영 이메일 전송 로직
}
public void logUserActivity(User user) {
// 유저 활동 로그 출력
}
}
한 클래스에 DB 저장/이메일 전송/로그 출력 등 여러 역할의 함수가 작성된 경우,
각 기능 변경 시 클래스 전체가 수정되어 다른 기능에 영향받을 수 있어 SRP 위반입니다.
그 결과, 유지보수 및 재사용이 어렵고 변경에 취약한 코드가 됩니다.
단일 책임 원칙 준수 예시
public class UserRepository {
public void saveUser(User user) {
// 데이터베이스에 유저 정보 저장 로직
}
}
public class EmailService {
public void sendWelcomeEmail(User user) {
// 유저에게 환영 이메일 전송 로직
}
}
public class UserActivityLogger {
public void logUserActivity(User user) {
// 유저 활동 로그 출력
}
}
각 역할의 함수를 각 클래스로 분리하는 것이 좋습니다.
개방-폐쇄 원칙 (OCP)
각 클래스가 확장에는 열려있고, 수정에는 닫혀 있어야 합니다.
개방-폐쇄 원칙 위반 예시
public class ReportGenerator {
public void generateReport(String type) {
if (type.equals("PDF")) {
// 리포트 PDF 파일 생성
} else if (type.equals("HTML")) {
// 리포트 HTML 파일 생성
}
}
}
한 클래스 문서 생성 함수 내에서 if문으로 파일 확장자에 따라 다른 코드가 작성된 경우,
새로운 확장자가 추가될 때마다 해당 함수를 수정해야 해서 OCP 위반입니다.
개방-폐쇄 원칙 준수 예시
public interface Report {
void generate();
}
public class PDFReport implements Report {
@Override
public void generate() {
// PDF 문서 파일 생성
}
}
public class HTMLReport implements Report {
@Override
public void generate() {
// HTML 문서 파일 생성
}
}
문서 생성 인터페이스 생성 후, 이를 상속받는 확장자별 문서 생성 클래스를 각각 생성하는 것이 좋습니다.
리스코프 치환 원칙 (LSP)
자식 클래스는 언제나 부모 클래스로 치환 가능해야 합니다.
리스코프 치환 원칙 위반 예시
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
// 클라이언트 (호출부)
public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly(); // 정상 동작
Bird penguin = new Penguin();
penguin.fly(); // 오류 발생
}
}
부모 클래스가 들어갈 자리에 자식 클래스가 들어가면 오류가 발생하여 LSP 위반입니다.
부모 클래스에서 정의한 모든 함수는 자식에서도 정상 동작해야 합니다.
리스코프 치환 원칙 준수 예시
public interface Flyable {
void fly();
}
public class Bird {
public void eat() {
System.out.println("Bird is eating");
}
}
/**
* 날 수 있는 클래스
*/
public class Sparrow extends Bird implements Flyable {
@Override
public void eat() {
System.out.println("Sparrows is eating");
}
@Override
public void fly() {
System.out.println("Sparrows is flying");
}
}
/**
* 날 수 없는 클래스
*/
public class Penguin extends Bird {
@Override
public void eat() {
System.out.println("Penguin is eating");
}
}
자식 클래스들에 선택적으로 물려줘야 하는 함수는 인터페이스로 분리해서 상속해야 합니다.
부모 클래스에서 정의하지 않은 함수는 자식 클래스가 구현하지 않아도 LSP 위반이 아닙니다.
인터페이스 분리 원칙 (ISP)
특정 클라이언트를 위한 인터페이스는 최소한으로 제공되어야 합니다.
하나의 범용 인터페이스보다 여러개의 구체적이고 작은 인터페이스로 나누는 것이 좋습니다.
클래스는 자신이 사용하지 않을 메소드를 구현하도록 강요받지 말아야 합니다.
인터페이스 분리 원칙 위반 예시
public interface Worker {
void work();
void eat();
}
public class Employee implements Worker {
@Override
public void work() {
System.out.println("Employee is woking");
}
@Override
public void eat() {
System.out.println("Employee is eating");
}
}
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot is woking");
}
@Override
public void eat() {
throw new UnsupportedOperationException("Robot do not eat");
}
}
// 클라이언트 (호출부)
public class Main {
public static void main(String[] args) {
Worker employee = new Employee();
employee.work(); // 정상 동작
employee.eat(); // 정상 동작
Worker robot = new Robot();
robot.work(); // 정상 동작
robot.eat(); // 오류 발생
}
}
인터페이스를 상속받은 클래스에서 불필요한 메소드를 구현하도록 정의하여, ISP 위반입니다.
인터페이스 분리 원칙 준수 예시
public Interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class Employee implements Workable, Eatable {
@Override
public void work() {
System.out.println("Employee is working");
}
@Override
public void eat() {
System.out.println("Employee is eating");
}
}
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot is working");
}
}
인터페이스도 클래스처럼 책임에 따라 분리해서 필요한 메소드만 상속받도록 합니다.
의존 역전 원칙 (DIP)
고수준 모듈이 저수준 모듈에 의존하지 않아야 합니다.
둘 다 추상화 (추상 클래스 또는 인터페이스) 에 의존해야 합니다.
의존 역전 원칙 위반 예시
/**
* 구체적인 동작을 직접 구현하는 저수준 모듈
*/
public class Fan {
public void spin() {
// 동작 실행 로직
}
public void stop() {
// 동작 멈춤 로직
}
}
/**
* 추상화된 함수를 제공하여 동작을 제어하는 고수준 모듈
*/
public class Switch {
private Fan fan;
public Switch(Fan fan) {
this.fan = fan;
}
public void turnOn() {
fan.spin();
}
public void turnOff() {
fan.stop();
}
}
고수준 모듈 Switch가 저수준 모듈 Fan에 의존적이므로 DIP 위반입니다.
저소준 모듈 함수명 및 매개변수가 변경되면, 고수준 모듈 실행부도 수정되어야 합니다.
스위치가 선풍기 외 다른 전자기기들을 다룰 수도 없으므로 좋은 설계가 아닙니다.
의존 역전 원칙 준수 예시
public interface Switchable {
void turnOn();
void turnOff();
}
/**
* 구체적인 동작을 직접 구현하는 저수준 모듈
*/
public class Fan implements Switchable {
@Override
public void turnOn() {
// 동작 실행 로직
}
@Override
public void turnOff() {
// 동작 멈춤 로직
}
}
/**
* 추상화된 함수를 제공하여 동작을 제어하는 고수준 모듈
*/
public class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
public void turnOn() {
device.turnOn();
}
public void turnOff() {
device.turnOff();
}
}
고수준 모듈이 저수준 모듈에 의존하지 않도록 모두 인터페이스에 의존합니다.
저수준 모듈 함수가 수정되어도 고수준 모듈 클래스에 영향을 주지 않게 됩니다.
고수준 모듈은 더 다양한 저수준 모듈 클래스들을 다룰 수 있게 됩니다.