객체 지향 프로그래밍을 하다 보면, 필연적으로 상속 구조를 활용하게 되는데요. 이 때, 서비스 로직에서 실제 인스턴스 타입별로 어떤 코드 실행을 다르게 해주어야 하는 경우가 발생합니다. 고민하지 않고 단순히 인스턴스 별 조건분기문으로 코드를 작성하게 되면, 새로운 요구사항이 추가될 때마다 코드의 변경이 많아질 수 있습니다. 또 그런 변경들이 프로그램의 로직을 복잡하게 만들어낼 수 있는데요. 오늘은 이런 상황에서 조건문을 사용하지 않고, 다형성을 활용할 수 있는 방안에 대해서 살펴보겠습니다. ## 상황 - 새를 나타내는 `Bird` 클래스가 있습니다. 이 `Bird` 객체를 API 응답으로 내려주기 위해서는 `BirdResult` 객체로 변형되어야 합니다.BirdResult 는 내부 객체와 API 응답 필드들을 분리하기 위하여 사용하고 있는 DTO 입니다. - 이 `BirdResult` 를 생성하기 위해 `Builder` 를 사용하고 있습니다. - `Bird` 로부터 `BirdResult` 의 각 응답 필드를 세팅하는 메서드는 아래와 같습니다. ``` // BirdResult.java /** * Bird 값을 주입한다. (As-is) * @param bird * @return Builder */ public Builder bird(Bird bird) { this.id = bird.getId(); this.name = bird.getName(); this.type = bird.getType(); // ... return this; } ``` ## 새로운 요구사항 - `Bird` 는 이제 새로운 하위타입들로 나누어져야 합니다. (`Canary` , `Duck` , `MockingBird` ) - 각 하위 타입에 따라서 `BirdResult` 에 채워주어야 할 필드가 달라집니다. 새로운 요구사항에 맞춰서, `Builder.bird(bird)` 메서드의 구현은 어떻게 달라져야 할까요? ## 타입을 통해 분기한다. - type 필드값이나, instanceof 를 통해 실제 객체 타입을 확인하여 분기할 수 있습니다. ``` // BirdResult.java /** * Bird 값을 주입한다. (Conditional) * @param bird * @return Builder */ public Builder bird(Bird bird) { this.id = bird.getId(); this.name = bird.getName(); this.type = bird.getType(); switch (bird.getType()) { case BirdType.Canary: this.feather = (Canary)bird.getColor(); case BirdType.Duck: this.beakSize = (Duck)bird.getBeakSize(); case BirdType.MockingBird: this.message = (MockingBird)bird.getMessage(); break; default: // ... } return this; } ``` 하지만 위와 같이 구현했을 때, 몇 가지 문제점을 생각해볼 수 있습니다. ### 타입이 새로 만들어질 때마다, 그에 맞는 케이스를 새로 추가해주어야 합니다. 위와 같은 코드가 한 곳에서만 사용되었다면, 당장은 문제가 되지 않을 수 있습니다. 하지만 코드는 계속 변경되고 추가되는 것이 문제죠. Bird 객체를 사용하는 모든 메서드를 찾아가면서 조건 분기로 되어 있는 모든 곳을 찾아서 알맞게 수정해주어야 하는 문제점이 있습니다. 새로운 코드가 추가되거나 변경이 될 때, 해당 코드를 사용하는 다른 모듈에서 모두 변경이 이루어져야 한다면, 이는 [개방-폐쇄 원칙 (Open-closed principle)](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle)을 위반한 코드가 됩니다. 따라서 변경이 되더라도, 사용하는 다른 모듈에서 변경하지 않아도 되도록 수정되어야 합니다. ### 다형성을 사용한 이유가 줄어들었습니다. 위 코드에서는 단지 다형성을 사용한 목적이 하나의 “타입” 값으로만 사용되고 있습니다. 그리고 그 타입을 통해 분기하여 다시 타입캐스팅을 하여 필요한 메서드를 일일이 호출하고 있습니다. 다형성의 성격 중 하나는 객체가 스스로 실행할 메서드를 실행 시점에서 결정할 수 있다는 것입니다. (이것을 동적 바인딩이라고 합니다.) 상위 클래스의 메서드를 재정의(overriding) 하면, 해당 메서드의 실행 시점에서 하위 클래스에서 재정의한 메서드를 실행하도록 결정됩니다. 그렇기 때문에 객체를 사용하는 모듈에서는 수정이 되더라도 변경을 할 필요가 없어지도록 구현할 수 있을 것입니다. ## 다중정의(overloading) 를 사용한다. - `.bird(bird)` 를 다중정의(overloading)하여 사용할 수 있습니다. ``` // BirdResult.java /** * Bird 값을 주입한다. (Overloading) * @param bird * @return Builder */ public Builder bird(Bird serverGroup) { this.id = bird.getId(); this.name = bird.getName(); this.type = bird.getType(); return this; } public Builder bird(Canary bird) { this.bird((Bird)bird) this.feather = (Canary)bird.getColor(); return this; } public Builder bird(MockingBird bird) { this.bird((Bird)bird) this.message = (MockingBird)bird.getMessage(); return this; } ``` 이렇게 구현했을 땐, 이제 받는 객체 타입에 따라서 다른 필드들이 세팅될 수 있습니다. 그리고 함께 세팅되어야 하는 공통 필드들에 대해서도 잘 설정될 수 있습니다. 하지만 아쉽게도, 위와 같은 구현 방식은 개방-폐쇄 원칙에 대해서 해결하지 못하고 있습니다. ### 새로운 타입이 만들어질 때마다, 새로운 메서드를 정의해주어야 한다. 위 `bird` 메서드를 호출하는 쪽에서, 어떤 타입인지 먼저 알고 있어야 하고, 타입에 따라 항상 타입 캐스팅을 해주어야 합니다. 그렇지 않으면 잘못된 메서드가 호출될 수 있습니다. 다시 말하면, 위 코드는 `Bird` 타입에 대한 분기를 `Builder` 를 호출하는 쪽으로 옮겨놓는 수준으로 생각할 수 있습니다. 아래 코드 예시를 통해 알아보겠습니다. ``` // SomethingService.java // 상위 타입인 Bird 로 받았습니다. 실제 객체가 어떤 타입인지는 알 수 없습니다. Bird bird = birdService.getBird(birdId); Builder builder = new Bird.Builder(); // 아래 메서드에서는 bird(Bird bird) 가 호출됩니다. 상위 타입으로 전달했기 때문입니다. builder.bird(bird); ``` ``` // SomethingService.java // 각 타입에 맞게 호출되기 위해서는 빌더 호출 시에 아래와 같은 분기가 필요합니다. if (bird instanceof Canary) { builder.bird((Canary)bird)); } else if (bird instanceof MockingBird) { builder.bird((MockingBird)bird)); } else { builder.bird(bird); } ``` 그렇다면, 다형성을 이용하여 위 코드를 구현하려면 어떻게 할 수 있을까요? ## Builder 에서 상위 타입의 메서드를 호출하도록 정의하고, 각 하위 타입에서 메서드를 재정의(Overriding) 한다. 타입에 따라 메서드의 구현이 달라져야 하는 경우에, 여러 하위 타입에서 메서드에 대해 재정의를 하면 일일이 조건문을 작성하지 않아도 다형적으로 호출되게 할 수 있습니다. 아래 처럼 상위 타입의 메서드를 호출하면, 각 하위 타입의 재정의된 메서드를 따라가도록 구현할 수 있습니다. 아래처럼 Builder 의 구현에서는 상위 타입의 `putResult(Builder)` 메서드를 호출합니다. `putResult` 에서는 전달한 `builder` 의 필드에 필요한 항목들을 주입해주는 역할을 해줍니다. ``` // BirdResult.java /** * Bird 값을 주입한다. (Polymorphism) * @param bird * @return Builder */ public Builder bird(Bird bird) { bird.putResults(this); // bird 에서 builder (this) 에 직접 주입한다. return this; } ``` 이 때 Builder 에서 구현되어 있던 필드 설정 부분들을 옮기기 위해 다음과 같이 변경합니다. - 상위 타입에서 호출될 메서드를 작성하여, 하위 타입에서 재정의할 수 있도록 합니다. - 각 하위 타입 별로 분기문에서 수행되었던 코드를 각 타입의 재정의한 메서드 안으로 옮깁니다. 즉, 각각의 `Canary`, `MockingBird` 타입에서는 추상 클래스 `Bird.putResult(Builder)` 에 대한 구현이 이루어져야 합니다. ``` // Bird.java public abstract class Bird { // ... /** * 서버 그룹 필드값들을 BirdResult.Builder 에 할당한다. * @param builder 응답 객체 빌더 */ public void putResult(BirdResult.Builder builder) { // 상위 타입에서 호출될 메서드를 작성하여, 하위 타입에서 재정의할 수 있도록 한다. builder.id(this.id); builder.name(this.name); builder.type(this.type); } } // Canary.java public class Canary extends Bird { // ... /** * 서버 그룹 필드값들을 BirdResult.Builder 에 할당한다. * - Bird.putResult 를 오버라이딩하여 동적바인딩 되도록 합니다. * * @param builder 응답 객체 빌더 */ @Override public void putResult(BirdResult.Builder builder) { // 공통 필드 세팅 super.putResult(builder); // Canary 필드 세팅 builder.feather(this.feather); } } // MockingBird.java public class MockingBird extends Bird { // ... /** * 서버 그룹 필드값들을 BirdResult.Builder 에 할당한다. * - Bird.putResult 를 오버라이딩하여 동적바인딩 되도록 합니다. * * @param builder 응답 객체 빌더 */ @Override public void putResult(BirdResult.Builder builder) { // 공통 필드 세팅 super.putResult(builder); // MockingBird 필드 세팅 builder.message(this.message); } } ``` 이렇게 구현을 하면, 다음과 같은 장점이 있습니다. - 타입에 따라 기능이 달라지는 여러 객체가 있을 때에도 일일이 조건문을 작성하지 않고, 다형적으로 호출되게 할 수 있습니다. - 새로운 타입이 추가되더라도, `putResult(Builder)` 만 알맞게 구현해준다면, `Builder.bird(Bird)` 를 변경하지 않고, 그대로 사용할 수 있습니다. 따라서 개방-폐쇄 원칙도 지킬 수 있게 되었습니다. 이 방법에 대해서 좀 더 자세하게 확인해보시고 싶으신 분들은 [**리팩터링(마틴 파울러)**](https://product.kyobobook.co.kr/detail/S000001810241)에서 **[조건문을 재정의로 전환 (Replace Conditional with Polymorphism)](https://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html)** 을 살펴보시면 좋을 것 같습니다. 유명한 리팩터링 항목이기 때문에, 단순 구글링을 하셔도 다양한 글들을 확인하실 수 있습니다.
# 툴체인 선택 표 1. 다양한 명령 집합에 대한 APP\_ABI 설정 | 아키텍처 | 툴체인 이름 | | ---------- | ------------------------------------ | | ARM 기반 | arm-linux-androideabi-**{gcc-version}** | | x86 기반 | x86-**{gcc-version}** | | MIPS 기반 | mipsel-linux-android-**{gcc-version}** | | ARM64 기반 | aarch64-linux-android-**{gcc-version}** | | X86-64 기반 | x86\_64-**{gcc-version}** | | MIPS64 기반 | mips64el-linux-android-**{gcc-version}** | # Sysroot 선택 ``` SYSROOT=$NDK/platforms/android-21/arch-arm ``` # 컴파일러 호출 ## 간단한 호출 다음은 NDK 내에 미리 빌드 되어있는 `arm-linux-androideabi-4.8` 툴체인을 이용한 빌드 방법이다. ``` export CC="$NDK/toolchains/arm-linux-androideabi-4.8/prebuilt/ \ linux-x86/bin/arm-linux-androideabi-gcc-4.8 --sysroot=$SYSROOT" $CC -o foo.o -c foo.c ``` 이 방법에서는 C++ STL (STLport, libc++ 또는 GNU libstdc++)을 사용할 수 없습니다. 예외나 RTTI가 지원되지도 않는다. ## 고급 방법 NDK는 명령줄에서 사용자 지정 툴체인 설치를 수행할 수 있는 `make-standalone-toolchain.sh` 셸 스크립트를 제공합니다. `$NDK/build/tools/` 디렉터리에 있으며, 여기서 $NDK는 NDK의 설치 루트 디렉터리입니다. ``` $NDK/bui...
댓글
댓글 쓰기