보다 더 나은 내일의 나를 위해

클래스를 래핑 한다는 것 본문

개발

클래스를 래핑 한다는 것

H-SC 2022. 8. 24. 15:54

서론

여러분은 프로그래밍을 하면서 '객체를 래핑 한다', '클래스를 래핑 한다'라는 말을 들어본 적 있나요? 만약 들어보지 못했다고 해도 괜찮습니다.

오늘은 클래스를 래핑 하는 방법과 사용하는 이유에 대해서 알아볼 것입니다.

 

 

Wrapping

wrapping을 직역하면 어떤 뜻인가요? 말 그대로 '싸다', '포장하다' 정도로 해석할 수 있습니다.

그렇다면 '클래스를 래핑 한다'라는 말은 어떻게 해석할 수 있을까요? 이 역시 클래스를 포장해서 사용한다고 볼 수 있습니다.

그럼 이 래핑은 어떻게 할까요?

 

기본적으로 다음과 같이 래핑 할 수 있을 것입니다.

class Food {
}

class FoodWrapper {
    private final Food food;
    
    public FoodWrapper() {
        this.food = new food;
    }
}

Food 클래스를 FoodWrapper 클래스로 포장했습니다.

이 방식을 사용하면 Food 클래스에서 제공하는 메소드를 사용하기 위해서는 위임을 통해 해결할 것입니다.

class Food {
    public String favor() {
        return "hot";
    }
}
class FoodWrapper {
    // ...
    public String favor() {
        food.favor();
    }
}

 

또한 상속을 받아 자식 클래스로써도 활용할 수 있을 것입니다.

class Food {
    public String favor() {
        return "hot";
    }
}

class FoodChild extends Food {
}

 

그렇다면 굳이 클래스를 래핑해서 사용하는 이유는 무엇일까요?

 

 

 

캡슐화

캡슐화를 통해서 구현 로직을 은닉할 수 있습니다.

만약 타겟 클래스에 인스턴스를 수정하는 메소드가 있고, 개발자가 해당 기능의 사용을 막고 싶을 때 사용할 수 있을 것입니다. 

class Food {
    public void 수정클래스() {
        // ...
    }
}
class FoodWrapper {
    // ...
    protected void 수정클래스() {
        food.수정클래스();
    }
}

 

 

기능의 확장성

또한 이렇게 사용한다면 필요에 따라 추가되어야 할 로직을 개발자가 손쉽게 다룰 수 있습니다.

이것은 위임을 통하면 메소드에 쉽게 기능을 추가할 수 있을 것입니다.

class Food {
    public void printKg(String s) {
        System.out.println(s);
    }
}
class FoodWrapper {
    // ...
    public void printKg(String s) {
        s += " Kg";
        food.printKg(s);
    }
}
class FoodChild extends Food {
    @Override
    public void printKg(String s) {
        s += " Kg";
        super.printKg(s);
    }
}

마치 프록시 패턴처럼 사용하는 것입니다.

 

또한 기존 메소드를 개발자의 편의성을 더 올려주도록 만들 수도 있습니다.

class Food {
    public void makeFood(String 재료1, String 재료2, String 재료3 ...) {
    	// ...
    }
}
class FoodIngredientDto {
    String 재료1;
    String 재료2;
    ...
}
class FoodWrapper {
    // ...
    public void makeFood(FoodIngredientDto ingredient) {
        // ...
    }
}

위 예시에서는 여러 인자를 따로 받는 makeFood() 메소드를 Dto 하나만을 활용해 해결할 수 있도록 하는 편의 메소드를 래퍼에 만들었습니다.

 

이 외에도 사용 방법에 따라 기능, 편의성 등을 추가할 수 있을 것입니다.

 

 

레이어 관리

이 생각은 다음 영상을 보며 생각했습니다.

토스ㅣSLASH 22 - 지속 성장 가능한 코드를 만들어가는 방법

아예 다른 레이어 또는 패키지에 있는 클래스를 래핑 클래스로 묶어서 관리한다면 래이어 침범을 막을 수 있습니다. 따라서 레이어 구성에도 용이할 것입니다.

또한 라이브러리 같은 아예 다른 패키지를 사용하는 유틸리티 클래스를 사용하는 상황에서, 비즈니스 로직을 작성하는 클래스에서 import 부분을 살펴볼 때 좀 더 비즈니스 종속적으로 관리할 수 있습니다.

 

 

 

종속성

마지막으로 종속성 관리입니다.

만약 현재 프로젝트에서 문자열을 변환해주는 유틸리티 라이브러리를 사용하고 있습니다. 보통 비즈니스 로직에 해당 유틸리티 라이브러리를 바로 사용했을 것입니다. 하지만 이 상황에선 라이브러리에 문제가 생겨 교체 혹은 직접 만든 로직을 사용하려고 할 때 비즈니스 로직 전체를 수정해야 하는 경우가 발생할 수 있습니다.

이런 상황을 방지하기 위해서 유틸리티 라이브러리를 래핑 해서 사용할 수 있습니다.

래핑을 사용한다면 기존 비즈니스 로직에서 사용하던 유틸리티 함수들의 명세는 그대로 두고, 문제가 되는 비즈니스 로직만 수정해 사용할 수 있습니다. 또는 라이브러리 교체를 쉽게 할 수도 있죠.

package ekfmsep;

// 외부 라이브러리
class Library {
    public String makeSomeFoods() {
        return "hamburger, pizza, chicken"
    }
}

 

package durl;

import ekfmsep.Library;

class Service {
    private Library library;
    public String cooking() {
        return library.makeSomeFoods();
    }
}

위 코드를 보면 ekfmsep에 존재하는 외부 라이브러리를 Service 내에서 사용하고 있습니다.

만약 해당 라이브러리의 makeSomFoods()에서 리턴해 주는 값이 한식이 필요하다고 피드백이 들어왔습니다. 따라서 한식을 리턴해 주는 또 다른 라이브러리로 교체하려고 합니다.

package aksgdlekfmsrj;

class KoreanFoodLibrary {
    public String getFoods() {
        return "bulgogi, yukgaejang"
    }
}

 

package durl;

import aksgdlekfmsrj.KoreanFoodLibrary;

class Service {
    private KoreanFoodLibrary library;
    public String cooking() {
        return library.getFoods();
    }
}

결국 aksgdlekfmsrj에 존재하는 KoreanFoodLibrary를 가져다 사용했습니다.

이 라이브러리를 사용하기 위해서 import 문도 바꾸고, 전역 변수 객체도 변경하고, 로직에서 함수 명도 변경하였습니다. 솔직히 이 정도 변경은 금방 할 수 있습니다. 하지만 이전 라이브러리를 사용하던 클래스가 많거나 이전 라이브러리의 함수를 많이 사용하고 있었다면 얼마나 수정해야 했을까요? 참 귀찮았을 것입니다.

 

그렇다면 이번엔 래핑을 해보겠습니다.

package lib;

import ekfmsep.Library;

class FoodLibrary {
    private Library library;
    public String makeSomeFoods() {
        return library.makeSomFoods();
    }
}

 

package durl;

import lib.FoodLibrary;

class Service {
    private Food library;
    public String cooking() {
        return Food.makeSomeFoods();
    }
}

Library를 래핑해서 FoodLibrary를 만들었습니다.

Service에서는 Library를 래핑 한 FoodLibrary를 사용해서 Library의 함수를 호출하고 있죠.

 

그럼 여기서도 KoreanFoodLibrary로 변경해봅시다.

package lib;

import aksgdlekfmsrj.KoreanFoodLibrary;

class FoodLibrary {
    private KoreanFoodLibrary library;
    public String makeSomeFoods() {
        return library.getFoods();
    }
}

 

package durl;

import lib.FoodLibrary;

class Service {
    private Food library;
    public String cooking() {
        return Food.makeSomeFoods();
    }
}

FoodLibrary라는 래핑 클래스는 수정되었지만, Service 클래스는 수정된 사항이 없습니다.

만약 위와 같이 한 번만 쓰거나 많이 쓰지 않는다면 굳이 래핑 클래스를 사용한다는 것은 낭비일 것입니다.

하지만 비즈니스 로직에서 여러 번 사용한다면 래핑 클래스를 사용했을 때 래핑 클래스만 수정하고, 비지니스 로직쪽은 전혀 수정할 부분이 없기 때문에 편의성과 확장성은 극대화될 것입니다.

 

 

 

두 가지 방식 중

앞에서 알아봤던 방식에는 2가지가 있었습니다. 그렇다면 어떤 방법을 사용하는 것이 좋을까요?

당연히 상황에 맞춰서 사용할 것입니다. 그럼 어떤 상황에서 사용하면 좋을지 봅시다.

 

필드 래핑

첫 번째로 소개한 래핑 방법입니다.

우선 이 방법을 사용하면 필요한 메소드만 제공할 수 있습니다.

개발 상황에서 꼭 모든 메소드를 제공해야 하는 것은 아닙니다. 따라서 메소드 사용에 제한을 하려고 할 때 필요한 메소드만 위임하면 되므로 손쉽게 제한할 수 있습니다.

또한 타겟 클래스의 필드에 대한 접근도 막을 수 있습니다. 클라이언트는 타겟 클래스를 래핑 한 클래스를 사용하기 때문에 래핑 클래스에서 따로 가능하게 해주지 않는 이상 타겟 클래스에 접근할 수 없게 됩니다.

 

필드 래핑 방법은 타겟 클래스에 접근을 제한하고 싶을 때 유용하게 사용될 것입니다.

 

상속

두 번째 방법인 상속입니다.

이 방법을 사용하게 되면 타겟 클래스에서 제공하는 메소드들을 그대로 상속받기 때문에 굳이 자식 클래스에 작성할 필요 없이 바로 사용할 수 있습니다.

또한 필요에 따라 오버라이딩을 통해 로직을 원하는 대로 수정할 수 있습니다. 필요에 따라 로직을 수정한다는 것은 필드 래핑 방법에서도 충분히 할 수 있습니다. 굳이 타겟 클래스에게 위임하는 것이 아닌 함수를 새로 작성하는 것으로 말이죠. 하지만 오버라이딩을 활용하는 것이 타겟 클래스에서 제공하던 기능이라는 것을 보다 더 직관적으로 알려줄 수 있습니다.

 

상속 방법은 타겟 클래스의 메소드를 손쉽게 제공하고 싶을 때 유용하게 사용할 수 있습니다.

 

 

 

 

 

만약 다른 의견이나 지적할 부분은 댓글로 알려주세요 :)

'개발' 카테고리의 다른 글

오버라이딩, 오버로딩 그 차이  (2) 2022.08.16
dependency 찾기  (0) 2022.07.30
제약있는 설계가 좋을까 나쁠까  (0) 2022.07.04
Comments