SOLID

이주형·2024년 10월 12일

SOLID 원칙은 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 나타냅니다.

📌 S - 단일 책임 원칙 (Single Responsibility Principle)

"클래스는 변경할 이유가 하나만 있어야 한다"는 개념을 강조합니다. 즉, 클래스는 하나의 핵심 기능에 집중해야 하며, 여러 가지 책임을 가지면 안 됩니다. 이는 코드의 유지보수성을 높이고, 변경 사항이 다른 기능에 미치는 영향을 최소화합니다.

안드로이드 앱에서 이 원칙을 적용할 때, 예를 들어 네트워크 통신, 데이터베이스 작업, UI 업데이트 등을 별도의 클래스로 분리할 수 있습니다.

나쁜 예시

// 이 예시에서 UserManager 클래스는 사용자 등록,
// 데이터베이스 저장, 이메일 전송 등 여러 책임을 가지고 있습니다.

class UserManager {
    fun registerUser(user: User) {
        // 사용자 등록 로직
        saveToDatabase(user)
        sendWelcomeEmail(user)
    }

    private fun saveToDatabase(user: User) {
        // 데이터베이스 저장 로직
    }

    private fun sendWelcomeEmail(user: User) {
        // 이메일 전송 로직
    }
}

좋은 예시

// 이 예시에서는 각 클래스가 단일 책임을 가지며, 
// UserManager는 이들을 조정하는 역할만 합니다.

class UserRegistration {
    fun registerUser(user: User) {
        // 사용자 등록 로직
    }
}

class UserDatabase {
    fun saveUser(user: User) {
        // 데이터베이스 저장 로직
    }
}

class EmailService {
    fun sendWelcomeEmail(user: User) {
        // 이메일 전송 로직
    }
}

// 사용 예
class UserManager(
    private val registration: UserRegistration,
    private val database: UserDatabase,
    private val emailService: EmailService
) {
    fun registerNewUser(user: User) {
        registration.registerUser(user)
        database.saveUser(user)
        emailService.sendWelcomeEmail(user)
    }
}

📌 O - 개방-폐쇄 원칙 (Open-Closed Principle)

"소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다"는 개념입니다. 이는 기존 코드를 변경하지 않고도 새로운 기능을 추가할 수 있어야 함을 의미합니다.

안드로이드에서 이 원칙을 적용할 때, 인터페이스나 추상 클래스를 사용하여 확장 가능한 구조를 만들 수 있습니다.

나쁜 예시

// 이 예시에서는 새로운 결제 방식을 추가할 때마다 
// PaymentProcessor 클래스를 수정해야 합니다.

class PaymentProcessor {
    fun processPayment(paymentType: String, amount: Double) {
        when (paymentType) {
            "CREDIT_CARD" -> processCreditCardPayment(amount)
            "PAYPAL" -> processPayPalPayment(amount)
            // 새로운 결제 방식을 추가할 때마다 이 메서드를 수정해야 함
        }
    }

    private fun processCreditCardPayment(amount: Double) {
        // 신용카드 결제 처리
    }

    private fun processPayPalPayment(amount: Double) {
        // PayPal 결제 처리
    }
}

좋은 예시

// 이 예시에서는 새로운 결제 방식을 추가할 때 PaymentMethod 인터페이스를 
// 구현하는 새 클래스만 만들면 되며, 기존 코드를 수정할 필요가 없습니다.

interface PaymentMethod {
    fun processPayment(amount: Double)
}

class CreditCardPayment : PaymentMethod {
    override fun processPayment(amount: Double) {
        // 신용카드 결제 처리
    }
}

class PayPalPayment : PaymentMethod {
    override fun processPayment(amount: Double) {
        // PayPal 결제 처리
    }
}

class PaymentProcessor {
    fun processPayment(paymentMethod: PaymentMethod, amount: Double) {
        paymentMethod.processPayment(amount)
    }
}

// 새로운 결제 방식 추가
class CryptoPayment : PaymentMethod {
    override fun processPayment(amount: Double) {
        // 암호화폐 결제 처리
    }
}

📌 L - 리스코프 치환 원칙 (Liskov Substitution Principle)

"프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다"는 개념입니다. 즉, 상위 클래스의 인스턴스를 하위 클래스의 인스턴스로 대체해도 프로그램이 여전히 정확하게 동작해야 합니다.

안드로이드에서 이 원칙을 적용할 때, 특히 상속 관계에서 주의해야 합니다.

나쁜 예시

// 이 예시에서 Square는 Rectangle의 행동을 위반합니다. 
// Rectangle의 너비와 높이를 독립적으로 설정할 수 있어야 하는데, 
// Square는 그렇지 않습니다.

open class Rectangle {
    open var width: Int = 0
    open var height: Int = 0

    open fun setWidth(width: Int) {
        this.width = width
    }

    open fun setHeight(height: Int) {
        this.height = height
    }

    fun area(): Int = width * height
}

class Square : Rectangle() {
    override fun setWidth(width: Int) {
        super.setWidth(width)
        super.setHeight(width)
    }

    override fun setHeight(height: Int) {
        super.setHeight(height)
        super.setWidth(height)
    }
}

좋은 예시

// 이 예시에서는 Rectangle과 Square가 공통 인터페이스 Shape를 구현하며, 
// 각각의 특성을 유지합니다.

interface Shape {
    fun area(): Int
}

class Rectangle(private val width: Int, private val height: Int) : Shape {
    override fun area(): Int = width * height
}

class Square(private val side: Int) : Shape {
    override fun area(): Int = side * side
}

📌 I - 인터페이스 분리 원칙 (Interface Segregation Principle)

"클라이언트가 자신이 사용하지 않는 인터페이스에 의존하도록 강제해서는 안 된다"는 개념입니다. 큰 인터페이스를 더 작고 구체적인 여러 인터페이스로 분리하여 클라이언트가 필요한 메서드만 알 수 있도록 합니다.

안드로이드에서 이 원칙을 적용할 때, 특히 리사이클러뷰의 어댑터나 프래그먼트 인터페이스 등에서 유용하게 사용할 수 있습니다.

나쁜 예시

// 이 예시에서 Robot은 먹거나 잠잘 필요가 없지만, Worker 인터페이스를 
// 구현하기 위해 불필요한 메서드를 구현해야 합니다.

interface Worker {
    fun work()
    fun eat()
    fun sleep()
}

class Human : Worker {
    override fun work() { /* 일하기 */ }
    override fun eat() { /* 먹기 */ }
    override fun sleep() { /* 잠자기 */ }
}

class Robot : Worker {
    override fun work() { /* 일하기 */ }
    override fun eat() { /* 불필요한 구현 */ }
    override fun sleep() { /* 불필요한 구현 */ }
}

좋은 예시

// 이 예시에서는 인터페이스가 분리되어 있어 Robot은 필요한 
// Workable 인터페이스만 구현하면 됩니다.

interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

class Human : Workable, Eatable, Sleepable {
    override fun work() { /* 일하기 */ }
    override fun eat() { /* 먹기 */ }
    override fun sleep() { /* 잠자기 */ }
}

class Robot : Workable {
    override fun work() { /* 일하기 */ }
}

📌 D - 의존성 역전 원칙 (Dependency Inversion Principle)

"고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다"는 개념입니다. 또한 "추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 한다"고 말합니다.

안드로이드에서 이 원칙을 적용할 때, 의존성 주입(Dependency Injection)을 사용하여 구현할 수 있습니다. Dagger나 Hilt 같은 라이브러리를 사용하거나, 간단한 경우 생성자 주입을 통해 구현할 수 있습니다.

나쁜 예시

// 이 예시에서 NotificationService는 구체적인 EmailService 클래스에 
// 직접 의존하고 있어, 다른 알림 방식으로 변경하기 어렵습니다.

class EmailService {
    fun sendEmail(message: String) {
        // 이메일 전송 로직
    }
}

class NotificationService {
    private val emailService = EmailService()

    fun sendNotification(message: String) {
        emailService.sendEmail(message)
    }
}

좋은 예시

// 이 예시에서 NotificationService는 추상화된 MessageService 인터페이스에 의존하며, 
// 구체적인 구현(이메일 또는 SMS)은 런타임에 주입됩니다. 
// 이로써 새로운 알림 방식을 쉽게 추가할 수 있고, 기존 코드 수정 없이 
// 알림 방식을 변경할 수 있습니다.

interface MessageService {
    fun sendMessage(message: String)
}

class EmailService : MessageService {
    override fun sendMessage(message: String) {
        // 이메일 전송 로직
    }
}

class SMSService : MessageService {
    override fun sendMessage(message: String) {
        // SMS 전송 로직
    }
}

class NotificationService(private val messageService: MessageService) {
    fun sendNotification(message: String) {
        messageService.sendMessage(message)
    }
}

이러한 SOLID 원칙들을 안드로이드 개발에 적용하면 코드의 유지보수성, 확장성, 테스트 용이성이 크게 향상됩니다. 각 컴포넌트의 책임을 명확히 하고, 느슨한 결합을 유지하며, 재사용 가능한 모듈을 만들 수 있습니다.

0개의 댓글