본문 바로가기
기타/자바 초급

SOLID 원칙이란

by 창이2 2022. 7. 3.

안녕하세요.

이번 포스팅은 SOLID 원칙에 대해 알아보려고 합니다.

SOLID 원칙은 객체지향설계시 지켜져야할 5가지 원칙으로 알려져 있는데 하나씩 알아보겠습니다.

 

1. 단일 책임의 원칙 : SRP (Single Responsibility Principle)

 

단일 책임의 원칙은 모든 클래스는 하나의 기능만 가진다 라는 의미입니다. 즉 클래스는 하나의 책임을 수행해야 한다는 뜻입니다.

코드로 예를 보면 

만약 게임플레이어중 일반 플레이어와 운영자 플에이어가 있을 때

Player라는 클래스 안에 운영자 기능과 일반 플레이 기능이 같이 있으면

유지보수가 힘들고 기능이 뒤섞여 있기 때문에 이를 하나의 책임만 갖는 클래스로 나누어야 하는 의미입니다.

더보기
//SRP 위배
class Player {

    val isAdmin = false

    fun ban(){
        if(isAdmin) doBan()
    }

}

class AdminPlayer {
    fun ban(){

    }
}

class NormalPlayer {

}

 

2. 개방폐쇄의 원칙 : OCP (Open Close Principle)

 

개방폐쇄의 원칙은 확장에는 열려있고 변경에는 닫혀 있어야 한다. 라는 원칙입니다. 즉 기존 구성요소는 변경이 일어나지 않아야 하며 쉽게 확장하여 재사용할 수 있어야 한다는 의미인데 

가령 attack() 이라는 메소드는 유지하며 attack() 내부의 들어가는 내용은 확장된 코드가 사용할 수 있다는 의미입니다.

개방폐쇄의 원칙중 핵심은 추상화와 다형성이라고 볼 수 있습니다.

아래처럼 공격이라는 개념을 추상화 하였고 추상화된 attack() 함수에 다형성을 갖고 있기 때문입니다.

 

더보기
//OCP 원칙 위배
class NormalPlayer {

    val spear = Spear()
    //Spear를 사용하면 Sword로 변경시 해당 사용부분 모두 변경
    fun attack(){
        spear.attack()
    }
}

class Spear {
    fun attack(){
        println("Spear")
    }
}

class Sword {
    fun attack(){
        println("Sword")
    }
}

/////////////////////////////////////////////////

class NormalPlayer {

    lateinit var attackInterface: AttackInterface 

    //확장에는 열려있고(다른 공격함수 사용) 수정(공격함수 수정)에는 닫혀있다.
  	fun setAttackInterface(newAttackInterface: AttackInterface){
	    attackInterface = newAttackInterface
    }
    
    fun attack(){
        //attackWithSword()
        attackInterface.attack()
    }

}

class Spear: AttackInterface {
    override fun attack(){
        println("Spear")
    }
}

class Sword: AttackInterface {
    override fun attack(){
        println("Sword")
    }
}

interface AttackInterface {
    fun attack()
}

 

3. 리스코브 치환의 원칙 : LSP (the Liskov Substitution Principle)

 

리스코브 치환 원칙은 부모 클래스의 기능을 자식 클래스도 수행할 수 있어야 한다는 원칙입니다.

예를 들면 NormalPlayer가 부모클래스이고 Warrior, Archer, Magician 이 자식 클래스일 때 NormalPlayer의 기능이 자식 클래스에게 모두 적용 될 수 있어야 한다는 원칙입니다.

만약 Warrior 클래스만 검/창 공격이 가능하다면 NormalPlayer에 있는  검/창 공격이 Archer, Magician에는 적용되지 않기 때문에 이 원칙에 위배되는 것입니다.  

 

더보기
open class NormalPlayer: AttackInterface {

    override fun attack() {
        attackWithPunch()
    }

    private fun attackWithPunch(){
        println("Punch")
    }
}

class Warrior: NormalPlayer() {

    private fun attackWithSword(){
        println("Sword")
    }

    private fun attackWithSpear(){
        println("Spear")
    }

    override fun attack() {
        attackWithSword()
//        attackWithSpear()
    }

}

class Archer: NormalPlayer(){
    private fun attackWithArrow(){
        println("Arrow")
    }
    override fun attack() {
        attackWithArrow()
    }
}

class Magician: NormalPlayer(){
    private fun attackWithWand(){
        println("Wand")
    }
    override fun attack() {
        attackWithWand()
    }
}

interface AttackInterface {
    fun attack()
}

 

4. 인터페이스 분리의 원칙 : ISP (Interface Segregation Principle)

 

인터페이스 분리의 원칙은 인터페이스를 구체적인 작은 단위로 분리시켜서 필요한 인터페이스만 상속받게 해야한다는 원칙입니다.

가령 AttackInterface에 공격을 막는 block() 이라는 함수가 있고 이는 Warrior클래스에서만 동작한다면 이는 Archer와 Magician 클래스에 불필요한 기능입니다.

그래서 해당 부분은 아예 BlockInterface를 두어 사용하는 것이 인터페이스 분리의 원칙이라고 보면 되겠습니다.

 

더보기
//ISP 원칙 위배
interface AttackInterface {
    fun attack()
    fun block() //모든 클래스에 적용 안됨
}

interface AttackInterface {
    fun attack()
}

interface BlockInterface {
    fun block()
}

 

5. 의존성 역전의 원칙 : DIP (Dependency Inversion Principle)

 

의존성 역전의 원칙은 추상화(abstract, interface)는 구체화(구체화된 class)에 의존하면 안되며 구체화된 클래스는 추상화에 의존해야 한다는 원칙입니다.

예를 들면 Warrior 클래스가 무기라는 객체를 갖고 있을 때 해당 무기를 구체화된 클래스로 갖고 있다면 변경이 일어날때 많은 작업이 필요하게 됩니다.

 

더보기
// 무기를 바꿀때 많은 변경이 필요
class Warrior : NormalPlayer() {

    val sword: Sword = Sword(10)
//    val spear: Spear = Spear(20)
    
    override fun attack() {
        println("Attack : ${sword.damage}")
//        println("Attack : ${spear.damage}")
    }

}

 

이를 추상화된 객체로 표현하면 아래와 같이 새로운 객체를 만들어서 다시 작업할 필요가 없어집니다. setWeapon을 통해 무기를 바꿔주기만 하면 되기 때문입니다.

더보기
class Warrior : NormalPlayer() {

    private var weapon: Weapon = Sword(damage = 10)

    fun setWeapon(weapon: Weapon){
        this.weapon = weapon
    }

    override fun attack() {
        println("Attack : ${weapon.damage}")
    }

}

data class Sword(override val damage: Int) : Weapon

data class Spear(override val damage: Int) : Weapon

interface Weapon {
    val damage: Int
}

 

또한 damage를 추상화 하여 공통적인 개념으로 묶었기 때문에 변경할 부분이 사라지게 됐습니다.

이렇게 구체적인 Sword나 Spear를 직접 구현하여 이용하는 것 보다 Weapon이라는 interface를 사용함으로써 많은 작업들이 사라지게 됩니다.

또한 setWeapon() 함수는 외부에서 Weapon을 주입하는데 이를 setter 방식의 의존주입(DI) 이라고 하는데 DIP 원칙을 지키면 DI의 효과까지 얻을 수 있게됩니다.

 

'기타 > 자바 초급' 카테고리의 다른 글

자바에서의 타입  (0) 2021.02.12
JVM이란  (0) 2021.02.06

댓글