JavaRush /Java Blog /Random-KO /코드를 더욱 깔끔하게 만드는 견고한 원칙
Paul Soia
레벨 26
Kiyv

코드를 더욱 깔끔하게 만드는 견고한 원칙

Random-KO 그룹에 게시되었습니다
솔리드란 무엇입니까? SOLID 약어의 약자는 다음과 같습니다. - S: 단일  책임 원칙. - O: 개방-폐쇄 원칙  . - L: 리스코프 대체 원리  . - I: 인터페이스 분리 원칙  . - D: 의존성 역전 원리  . SOLID 원칙은 모듈 설계 방법을 조언합니다. 애플리케이션이 구축되는 벽돌입니다. 원칙의 목적은 다음과 같은 모듈을 설계하는 것입니다. - 변화 촉진 - 이해하기 쉬움 - 재사용 가능 이제 예제를 통해 모듈이 무엇인지 살펴보겠습니다.
class MainRepository(
    private val auth: FirebaseAuth
) {
    suspend fun loginUser(email: String, password: String) {
        try {
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            val file = File("errors.txt")
            file.appendText(text = e.message.toString())
        }
    }
}
이 예에서 무엇을 볼 수 있습니까? FirebaseAuth는 생성자에 전달되며 이를 사용하여 로그인을 시도합니다. 응답으로 오류가 발생하면 메시지를 파일에 씁니다. 이제 이 코드에 어떤 문제가 있는지 살펴보겠습니다. 1. S: 단일 책임 원칙  : 클래스(또는 함수/메서드)는 한 가지만 책임져야 합니다. 클래스가 여러 문제를 해결해야 하는 경우 이러한 문제에 대한 솔루션을 구현하는 하위 시스템이 서로 연결됩니다. 그러한 하위 시스템 중 하나가 변경되면 다른 하위 시스템도 변경됩니다. 이 예에서 loginUser 함수는 로그인과 파일에 오류 쓰기라는 두 가지 작업을 담당합니다. 하나의 책임이 존재하려면 오류를 기록하는 로직을 별도의 클래스에 배치해야 합니다.
class FileLogger{
    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }
}
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {
    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }
}
오류 기록 논리는 별도의 클래스로 이동되었으며 이제 loginUser 메소드에는 권한 부여라는 단 하나의 책임만 있습니다. 오류 로깅 논리를 변경해야 하는 경우 loginUser 2 함수가 아닌 FileLogger 클래스에서 이를 수행합니다. O: 개방형  원칙: 확장을 위해 소프트웨어 엔터티(클래스, 모듈, 함수)를 열어야 합니다. 하지만 수정용은 아닙니다. 이러한 맥락에서 확장에 대한 개방성은 필요에 따라 클래스, 모듈 또는 기능에 새로운 동작을 추가할 수 있는 능력이고, 변경에 대한 폐쇄성은 소프트웨어 엔터티의 소스 코드 변경을 금지하는 것입니다. 언뜻 보면 이 말은 복잡하고 모순적으로 들립니다. 하지만 따져보면 그 원리는 꽤 논리적이다. OCP 원칙에 따르면 기존 코드를 변경하는 것이 아니라 새로운 코드를 추가하여 소프트웨어가 변경됩니다. 즉, 원래 생성된 코드는 "손상되지 않고" 안정적으로 유지되며 구현 상속이나 추상 인터페이스 및 다형성의 사용을 통해 새로운 기능이 도입됩니다. 이제 FileLogger 클래스를 살펴보겠습니다. 다른 파일에 로그를 작성해야 하는 경우 이름을 변경할 수 있습니다.
class FileLogger{
    fun logError(error: String) {
        val file = File("errors2.txt")
        file.appendText(text = error)
    }
}
그러나 모든 로그는 새 파일에 기록되며 대부분 필요하지 않습니다. 하지만 클래스 자체를 변경하지 않고 어떻게 로거를 변경할 수 있습니까?
open class FileLogger{

    open fun logError(error: String) {
        val file = File("error.txt")
        file.appendText(text = error)
    }

}

class CustomErrorFileLogger : FileLogger() {

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
FileLogger 클래스와 logError 함수를 open으로 표시하고, 새로운 CustomErrorFileLogger 클래스를 생성하고, 새로운 로깅 구현을 작성합니다. 결과적으로 우리 클래스는 기능 확장이 가능하지만 수정은 닫혀 있습니다. 3. L: 리스코프 대체 원칙  . 하위 클래스는 상위 클래스를 대체할 수 있어야 합니다. 이 원칙의 목적은 프로그램을 중단하지 않고 파생된 상위 클래스 대신 하위 클래스를 사용할 수 있다는 것입니다. 코드가 클래스 유형을 확인하는 것으로 밝혀지면 대체 원칙을 위반한 것입니다. 후속 클래스를 다음과 같이 작성한다면:
class CustomErrorFileLogger : FileLogger() {

    fun customErrorLog(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
이제 저장소에서 FileLogger 클래스를 CustomErrorFileLogger로 바꾸겠습니다.
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: CustomErrorFileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
이 경우 logError는 상위 클래스에서 호출됩니다. 그렇지 않으면 fileLogger.logError(e.message.toString())에 대한 호출을 fileLogger.customErrorLog(e.message.toString())으로 변경해야 합니다. :  인터페이스 분리 원칙 특정 클라이언트를 위해 설계된 고도로 전문화된 인터페이스를 만듭니다. 클라이언트는 사용하지 않는 인터페이스에 의존해서는 안 됩니다. 복잡해 보이지만 실제로는 매우 간단합니다. 예를 들어 FileLogger 클래스를 인터페이스로 만들고 여기에 함수를 하나 더 추가해 보겠습니다.
interface FileLogger{

    fun printLogs()

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
이제 모든 하위 클래스에서 필요하지 않더라도 모든 하위 항목은 printLogs 함수를 구현해야 합니다.
class CustomErrorFileLogger : FileLogger{

    override fun printLog() {

    }

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
이제 우리는 빈 함수를 가지게 되는데, 이는 코드 청결성에 좋지 않습니다. 대신 인터페이스에서 기본값을 만든 다음 필요한 클래스에서만 함수를 재정의할 수 있습니다.
interface FileLogger{

    fun printLogs() {
        //Howая-то дефолтная реализация
    }

    fun logError(error: String) {
        val file = File("errors.txt")
        file.appendText(text = error)
    }

}
class CustomErrorFileLogger : FileLogger{

    override fun logError(error: String) {
        val file = File("my_custom_error_file.txt")
        file.appendText(text = error)
    }

}
이제 FileLogger 인터페이스를 구현할 클래스가 더 깔끔해졌습니다. 5. D: 종속성 반전 원리  . 종속성의 객체는 구체적인 것이 아닌 추상화여야 합니다. 메인 클래스로 돌아가 보겠습니다.
class MainRepository(
    private val auth: FirebaseAuth,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        } catch (e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
여기에서는 이미 모든 것을 구성하고 수정한 것 같습니다. 그러나 아직 변경해야 할 점이 하나 더 있습니다. FirebaseAuth 클래스를 사용하고 있습니다. 어느 시점에서 승인을 변경하고 Firebase를 통하지 않고 예를 들어 일종의 API 요청을 사용하여 로그인해야 한다면 어떻게 될까요? 그러면 우리는 많은 것을 바꿔야 할 것인데, 우리는 그것을 원하지 않을 것입니다. 이렇게 하려면 signInWithEmailAndPassword(이메일: 문자열, 비밀번호: 문자열) 함수를 사용하여 인터페이스를 만듭니다.
interface Authenticator{
    fun signInWithEmailAndPassword(email: String, password: String)
}
이 인터페이스는 우리의 추상화입니다. 이제 우리는 로그인의 구체적인 구현을 만듭니다.
class FirebaseAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        FirebaseAuth.getInstance().signInWithEmailAndPassword(email, password)
    }

}
class CustomApiAuthenticator : Authenticator{

    override fun signInWithEmailAndPassword(email: String, password: String) {
        //другой способ логина
    }

}
그리고 `MainRepository` 클래스에는 이제 특정 구현에 대한 의존성이 없고 추상화에만 의존합니다.
class MainRepository (
    private val auth: Authenticator,
    private val fileLogger: FileLogger,
) {

    suspend fun loginUser(email: String, password: String) {
        try{
            auth.signInWithEmailAndPassword(email, password)
        }catch(e: Exception) {
            fileLogger.logError(e.message.toString())
        }
    }

}
그리고 이제 인증 방법을 변경하려면 모듈 클래스에서 한 줄만 변경하면 됩니다.
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION