ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 인터페이스와 제네릭을 이용한 공통 로직 관리
    Java & Kotlin 2024. 1. 1. 23:08
    반응형

    1) 서론

    하나의 저울로 모든 종류의 물질들을 측정할 수 있을까요? 

     

    요리를 할 때 그램 단위의 정밀한 측정이 필요할 때는 미세한 측정이 가능한 요리용 저울이 필요할 것입니다. 하지만 사람의 몸무게를 측정할 때는 당연히 큰 저울이 필요합니다. 미세한 그램 단위까지 알 필요도 없는데요. 만약 건축 자재와 같이 큰 것들은 더더욱 다른 저울로 측정되어야 할 것입니다.

     

    하지만 만약에 요리할 때 사용하면서, 내 몸무게와 코끼리의 몸무게도 재려고 한다면 세 개의 저울이 필요할 것입니다. 저울 한 개가 모든 용도에 맞게 사용되면 아주 간편할 것 같은데요. 결국 '무게 측정 행위'는 동일한 것 같습니다.

     

    개발에서도 마찬가지입니다. 여러 도메인 혹은 비즈니스 로직에서 동일한 로직을 사용한다면, 공통화하고 싶은 욕구가 생깁니다. 마치 저울 한 개로 모든 무게를 측정하듯이, 무언가 하나로 해결해보고 싶어 지는데요.

     

    이번 글에서는 다양한 타입을 가진 도메인 객체의 로직을 공통화하는 것을 공유드립니다.

     

     


    2) 상황

    각기 다른 햄버거 세 개가 있습니다. 각 햄버거들의 재료들은 무게를 가지고 있는데요.

    // 예시의 무게를 표현하기 위해 Long 타입 사용
    class 치즈버거(
        val 치즈: Long,
        val 패티: Long,
        val 치즈빵: Long
    )
    
    class 토마토버거(
        val 치즈: Long,
        val 토마토: Long,
        val 패티: Long,
        val 참깨빵: Long
    )
    
    class 치킨버거(
        val 치킨: Long,
        val 치킨소스: Long,
        val 양상추: Long,
        val 얇은빵: Long
    )

     

    요리를 할 때마다 햄버거의 재료 양이 변하지 않는지 측정하고 싶습니다. 재료의 양이 이전과 다르게 큰 폭으로 변한다면 요리가 달라집니다.

    • 이전에 만든 햄버거 재료 대비 변동폭 확인
    • 변동폭이 기준치를 초과할 경우 알림 발송
    // 예시의 무게를 표현하기 위해 Long 사용
    class 치즈버거(
        val 치즈: Long,
        val 패티: Long,
        val 치즈빵: Long
    ) {
        // 이전 햄버거와 무게 차이 비교 (예시를 위해 if문 대략 사용)
        fun weightDiscrepancy(original치즈버거: 치즈버거): Long {
            val 치즈무게차이 = original치즈버거.치즈.minus(this.치즈)
            val 패티무게차이 = original치즈버거.패티.minus(this.패티)
            val 치즈빵무게차이 = original치즈버거.치즈빵.minus(this.치즈빵)
            
            if (치즈무게차이 > 3) return 치즈무게차이
            if (패티무게차이 > 3) return 패티무게차이
            if (치즈빵무게차이 > 3) return 치즈빵무게차이
            
            return 0
        }
    }
    
    // 토마토버거 class 생략
    fun weightDiscrepancy(original토마토버거: 토마토버거): Long {
            val 치즈무게차이 = original토마토버거.치즈.minus(치즈)
            val 토마토무게차이 = original토마토버거.토마토.minus(토마토)
            val 패티무게차이 = original토마토버거.패티.minus(패티)
            val 참깨빵무게차이 = original토마토버거.참깨빵.minus(참깨빵)
    
            if (치즈무게차이 > 3) return 치즈무게차이
            if (토마토무게차이 > 3) return 토마토무게차이
            if (패티무게차이 > 3) return 패티무게차이
            if (참깨빵무게차이 > 3) return 참깨빵무게차이
    
            return 0
    }
    
    // 치킨버거 class 생략
    fun weightDiscrepancy(original치킨버거: 치킨버거): Long {
            val 치킨무게차이 = original치킨버거.치킨.minus(치킨)
            val 치킨소스무게차이 = original치킨버거.치킨소스.minus(치킨소스)
            val 양상추무게차이 = original치킨버거.양상추.minus(양상추)
            val 얇은빵무게차이 = original치킨버거.얇은빵.minus(얇은빵)
    
            if (치킨무게차이 > 3) return 치킨무게차이
            if (치킨소스무게차이 > 3) return 치킨소스무게차이
            if (양상추무게차이 > 3) return 양상추무게차이
            if (얇은빵무게차이 > 3) return 얇은빵무게차이
    
            return 0
    }
    • 이전에 만든 햄버거, 현재 만들고 있는 햄버거 재료들의 무게를 비교
    • 만약 3그램 이상 변동 시 변동 무게 return, 없다면 0

    • 비즈니스 로직을 다루는 서비스 클래스
    • 기존 햄버거, 새로운 햄버거 조회하여 비교
    • 3그램보다 크다면 알림 발송

    3) 문제점

    중복이 많이 보입니다.

     

    비교하는 클래스 타입은 다르지만, 동일한 로직이 세 번이나 중복됩니다.

    • if문
    • 버거 무게 변동폭 비교
    • 알림 발송하는 sendAlert() 함수

     

    각 햄버거는 단순히 종류의 차이만 있을 뿐 동일한 비지니스 로직을 가지고 있습니다. 이것을 공통 메서드로 분리해 보겠습니다.

        fun sendAlertIfHasDiscrepancy(타입은 어떤걸로 ???) {
            // 햄버거 무게 확인
            
            // 만약 차이나면 알림 발송
        }

     

    햄버거 무게 확인, 차이 발생 시 알림 발송의 공통 로직을 가진 함수입니다. 하지만 각 햄버거의 클래스 타입은 다릅니다. 결국 함수 세 개를 만들어야 하는 걸까요?

        fun sendAlertIfHasDiscrepancy(origin치즈버거, new치즈버거) {
            // 치즈버거 무게 확인
            
            // 만약 차이나면 알림 발송
        }
        
        fun sendAlertIfHasDiscrepancy(origin토마토버거, new토마토버거) {
            // 토마토버거 무게 확인
            
            // 만약 차이나면 알림 발송
        }​

    3) 개선

    각 햄버거 클래스의 weightDiscrepancy()는 공통되는 로직입니다. 이것을 인터페이스를 이용해서 한 단계 추상화 합니다.

    interface 햄버거 {
    
        // 파라미터로 여러 타입의 햄버거를 받아야 하는 문제 (기존)
        fun weightDiscrepancy(어떤 타입으로 ???): Long
         
        // 제네릭으로 유연하게 (변경)
        fun<T> weightDiscrepancy(burger: T): Long
    }

    • 파라미터로 들어오는 타입이 각기 다른 문제 → 제네릭으로 유연하게 수정
    • 함수 내부에서 casting
    class serviceClass {
    
       // 기존의 중복되는 로직 
       fun 치즈버거로직() {
           val original치즈버거 = 치즈버거service.find()
           val new치즈버거 = 치즈버거service.find()
            if (new치즈버거.weightDiscrepancy(original치즈버거) > 3) {
        	    sendAlert()
            }
       }
    
       // 기존의 중복되는 로직 
       fun 토마토버거로직() {
           val original토마토버거 = 토마토버거service.find()
           val new토마토버거 = 토마토버거service.find()
            if (new토마토버거.weightDiscrepancy(original토마토버거) > 3) {
        	    sendAlert()
            }
       }
    
       // 기존의 중복되는 로직 
       fun 치킨버거로직() {
           val original치킨버거 = 치킨버거service.find()
           val new치킨버거 = 치킨버거service.find()
            if (new치킨버거.weightDiscrepancy(original치킨버거) > 3) {
        	    sendAlert()
            }
       }      
    }

    • 햄버거 인터페이스를 이용하여 확장함수를 작성
    • 제네릭 타입의 파라미터를 이용하여 어떠한 햄버거라도 파라미터 가능

     

    기존에 반복되는 if문을 확장함수, 제네릭, 인터페이스를 통해서 해결했습니다. 장점은 이렇습니다.

    • 공통 로직을 한 곳에서 관리 가능
    • 핵심 validation 로직에 대한 응집도 향상
    • 핵심 비즈니스 로직을 단순하고 간결하게 유지 가능
    • 햄버거 종류의 클래스는 햄버거를 구현하여 validation 로직 추가 강제

    4) 결론

    꼭 이런 방법을 사용하지 않고 리플랙션을 이용해서 각 필드의 값을 비교하는것도 가능한데요. 하지만 코드에서 도메인을 나타내기 어려우며, 시간이 흘렀을 때 파악하기 어렵다고 생각합니다.

     

    코드는 작게 유지하는 것이 아주 중요합니다. 지금 당장 필요하지 않은 것을 혹시나 하는 마음에 구현하는 것은 가독성을 떨어뜨리고, 다른 개발자에게 혼란을 줍니다. 

     

    하지만 현실 비즈니스 요건이 추가될수록 로직은 커지고 지저분해질 수 밖에 없습니다. 지저분해질수록 비지니스 로직을 유지보수 하기 어려워지고, 잘못된 구현이 발생할 수도 있습니다. 그래서 아주 작고 단순하게 유지하려는 노력은 중요한데요.

     

    흩어진 비지니스 로직 중 공통화할 수 있는 것을 찾고, 이것을 한 곳에서 관리하는 것이 중요합니다. 그렇지 않다면 동일한 요건을 가졌지만, 일부에만 적용되고 누락되는 사고를 방지할 수도 있다고 생각합니다.

    반응형

    댓글

Designed by Tistory.