본문 바로가기
디자인 패턴

if else가 많을 때 사용할 수 있는 패턴

by zinozino 2023. 4. 21.

개발을 하다 보면 어떠한 조건을 체크하는 경우가 있는데, 조건이 너무 많으면, if 문이 길어질 수 있습니다.

if문은 처리하는 속도는 빠르지만 길어질 경우 가독성이 떨어지고, 유지보수가 어려운 점이 있습니다.

 

 

아래 예시는 새로운 타입이 추가될 때마다 else if문으로 추가해야 합니다. 

func checkMediaType(type: String) -> String {
    var result = ""
    if type == "Picture"  {
        result = "This media type is Picture"
    } else if type == "Video" {
        result = "This media type is Video"
    } else if type == "Text" {
        result = "This media type is Text"
        
    } else if type == "Audio" {
        result = "This media type is Audio"
    } else if type == "Link" {
        result = "This media type is Link"
        
    } else if type == "Streaming" {
        result = "This media type is Streaming"
    } else {
        result = "Can't check Type!"
    }
    return result
}

 

좀 더 가독성을 높혀보겠습니다. 자주 쓰는 도구인 Enum을 통해서 코드를 고쳐 보겠습니다.

enum MediaType: String {
    
    case text = "Text"
    case picture = "Picture"
    case video = "Video"
    case audio = "Audio"
    case link = "Link"
    case streaming = "Streaming"
    case notChecked = ""
}

func checkMediaType(type: MediaType) -> String {
    var result = ""
    switch type {
    case .text, .video, .picture, .streaming, .link, .audio:
        result = "This media type is \(type.rawValue)"
    default:
        result = "Can't check Type!"
    }
    return result
}

 

enum으로 파라미터를 받아서 잘못된 값을 컨파일러 타임에 알아낼 수 있으니, 안정성도 높아졌고, case 가 늘어나도 result의 형식이 같으니 코드가 길어질 일도 없고 case에 추가만 하면 되니 유지보수도 좋아졌다고 말할 수 있을 것 같습니다.

 

만약 여기서 result의 형식이 "This media type is \(type.rawValue)" 이 아니라면 따로 케이스 분기를 처리해야합니다. 한 두 개 정도의 따른 처리는 아직 괜찮습니다.

 

func checkMediaType(type: MediaType) -> String {
    var result = ""
    switch type {
    case .text, .picture, .link, .audio:
        result = "This media type is \(type.rawValue)"
    
    case .streaming, .video:
        result = "This media type is Combined \(type.rawValue)"
    default:
        result = "Can't check Type!"
    }
    return result
}

 

여기서 모든 타입이 다른 처리를 한다면 어떨까요? 

모두 다른 행동을 처리한다면 Switch문도 장점도 사라지게 됩니다.

func executeMediaType(type: String) -> String {
    var result = ""
    
    if type ==  "Picture"  {
        result = "Show Picture Upload"
    } else if type == "Video" {
        result = "Play Video"
    } else if type == "Text" {
        result = "Print Text"
    } else if type == "Audio" {
        result = "Mute Audio"
    } else if type == "Link" {
        result = "Tap Link"     
    } else if type == "Streaming" {
        result = "Chatting Streaming "
    } else {
        result = "Can't check Type!"
    }
    return result
}

 

여기서 저는 if 문을 하나의 단위로 보겠습니다.

 

“Picture면 처리, 아니면 다른 조건문, 마지막 아무것도 없을 때는 없는 것으로 처리”

 

protocol MediaTpyeCondition {
    var nextCondition: MediaTpyeCondition?
    func executeCondition(by type: MediaType) -> String?
    @discardableResult
    func chain(nextCondition: MediaTpyeCondition) -> MediaTypeCondition
}

 

1.  자신의 조건이 아니라면 다음 조건을 호출하기위한 저장프로퍼티입니다.

2.  자신의 조건코드를 실행시킬 메서드입니다.

3.  자신의 다음 조건을 연결 시킬 수 있는 메서드입니다.(1번의 프로퍼티에 넣어서 연결해 주게 됩니다.)

 

 

다음은  MediaTpyeCondition프로토콜을 따르는 구체적인 객체를 정의하겠습니다.

 

final class PictureCondition: MediaTypeCondition {

    var nextCondition: MediaTypeCondition?
    
    func executeCondition(by type: MediaType) -> String? { 
		if case .picture = tpye {
      		return "Show Picture Upload"
      	}
        //조건에 맞지 않다면 chainning된 다른 MediaTypeCondition으로 전달
        return self.nextCondition?.executeCondition(by: type)
    }
    
    @discardableResult
    func chain(nextCondition: MediaTypeCondition) -> MediaTypeCondition {
    	self.nextCondition = nextCondition
        return nextCondition
    }
}

Picture에 대한 처리문이 하나의 타입으로 정의되었습니다. 

나머지도 같은 방식으로 정의하면  아래처럼 체이닝으로 조건을 추가하게 됩니다. 

let startCondition = VideoTypeCondition()

startCondition
    .chain(condition: TextCondition())
    .chain(condition: LinkCondition())
    .chain(condition: PictureCondition())
    .chain(condition: AudioCondition())
    .chain(condition: StreamingCondition())
    .chain(condition: NotCheckedCondition())

 

이제  startCondition를 실행 시킬 역할도 분리하여  만들어 주도록 하겠습니다.

final class ConditionExecutor {
    var startCondition: MediaTypeCondition
    
    init(startCondition: MediaTypeCondition) {
    self.startCondition = startCondition
    }
    
    fuc execute(mediaType: MediaType) -> String? {
    	return startCondition.exectuteCondition(by: mediaType)
    }
}

시작 조건을 받고 해당 시작조건을 실행 시키는 객체를 만들었습니다. 

 

let conditionExecutor = ConditionExecutor(startCondition: startCondition)
print(conditionExecutor.excute(mediaType: .picture))
print(conditionExecutor.excute(mediaType: .text))

 

해당 예제는 String을 반환하지만, 반환이 필요하지 않은 곳에서는 좀 더 간편하게 사용할 수 있습니다.

 

 

마무리


 

장점

  • 객체단위로 조건을 처리할 수 있다.
  • chain의 순서의 따라 조건의 순서를 정할 수 있다.(Director같은 패턴을 사용하면 편리!)
  • 밖에서 보여지는 코드가 간결하다.

 

단점

  • 구현해야하는 코드의 양이 많아진다.
  • 적은 조건이 있는 곳에서는 자칫 배보다 배꼽이 더 커질 수 있다.

 

개인적으로 조건문이 많이 있는 곳에서는 사용하는 것보다

조건문과 조건문에서 처리하는 로직이 많을 때 분리하는 용도로 사용할 때 좋았습니다.