기본 콘텐츠로 건너뛰기

다형성을 사용하도록 조건문 다시 작성하기



객체 지향 프로그래밍을 하다 보면, 필연적으로 상속 구조를 활용하게 되는데요. 이 때, 서비스 로직에서 실제 인스턴스 타입별로 어떤 코드 실행을 다르게 해주어야 하는 경우가 발생합니다.

고민하지 않고 단순히 인스턴스 별 조건분기문으로 코드를 작성하게 되면, 새로운 요구사항이 추가될 때마다 코드의 변경이 많아질 수 있습니다. 또 그런 변경들이 프로그램의 로직을 복잡하게 만들어낼 수 있는데요. 오늘은 이런 상황에서 조건문을 사용하지 않고, 다형성을 활용할 수 있는 방안에 대해서 살펴보겠습니다.

## 상황
-   새를 나타내는 `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)** 을 살펴보시면 좋을 것 같습니다. 유명한 리팩터링 항목이기 때문에, 단순 구글링을 하셔도 다양한 글들을 확인하실 수 있습니다.


댓글

이 블로그의 인기 게시물

안드로이드 native c++ 컴파일 관련 정리

# 툴체인 선택 표 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...

정보 검색 평가 지표 ( + RAGAS)

> https://amitness.com/posts/information-retrieval-evaluation 글을 읽고 정리한 문서입니다. ## 지표의 목적 상위 N 결과가 얼마나 우수한지 어떻게 평가할 것 인가? ### Binary relevance - 문서에 대한 관련성을 `있다 / 없다` 로만 판단한다. - 현재 Ranking model 이 query 에 대해서 5개의 각각의 문서 관련도는 `[1, 0, 1, 0, 1]` 로 나타낼 수 있다. (*binary*) ## Order-unaware metrics ### Precision@k $$ Precision@k = \frac{ true\ positives@k}{(true\ positives@k) + (false\ positives@k)} $$ - 이 메트릭은 상위 K 결과의 관련 항목 수를 정량화합니다. - 추출된 k 랭크 문서 중에서 관련 있는 문서의 갯수 예시) *Precision@2* ### Recall@k $$ Recall@k = \frac{ true\ positives@k}{(true\ positives@k) + (false\ negatives@k)} $$ - 이 메트릭은 쿼리에 대한 모든 실제 관련 결과 중에서 몇 개의 실제 관련 결과가 표시되었는지 알려줍니다. - 전체 관련 있는 문서 갯수 중에서 k 랭크 내에 추출된 관련 있는 문서의 갯수 예시) *Recall@2* ### 참고: Precision 과 Recall 의 집합관계 - A = 모델에서 문서가 관련 있다고 예측한 영역 (예측) - B = 실제 관련 있는 문서가 있는 영역 (정답) - b 영역 = True Positive 로 모델이 추출한 관련 문서 중 실제 관련 있는 문서가 있었던 영역 모델이 반환한 결과 중에서 실제 관련도 있는 문서를 추출한 비율이 precision, 실제 관련 있는 문서 목록 중 model 이 올바르게 문서를 추출한 비율이 recall 이라고 할 수 있다...

서버에서 Client IP 를 추출하는 여러가지 방법

서비스 요구사항에 따라 Client IP 가 필요한 상황이 있다. 보안을 위해서 Client IP 를 확인하여 접근을 허용할 수 있다. 허용되지 않은 IP 의 경우 접근을 막을 수 있다. 로그 요구사항으로 어떤 사용자가 접근하고 있는 지를 기록하기 위해 Client IP 를 남겨야 할 수 있다. 하지만 사용자나 서비스의 네트워크 구성에 따라서 Client IP 를 추출하는 것이 쉽지 않을 수 있다. 프록시가 있어 직접 연결한 Client 를 실제 사용자로 판단할 수 없는 경우가 그렇다. 프록시 뒤에 있는 사용자를 찾으려고 노력하면 Client IP 를 숨기거나 우회하기 위해서 변조를 시도하는 상황을 마주하게 된다. 그래서 Client IP 를 추출하기 위한 여러 방안들을 아래에 정리하게 되었다. 결론부터 먼저 말하면, 일반적인 상황에서 나는 `X-Forwarded-For` 의 가장 오른쪽의 Public IP 를 Client IP 로 판단하기로 했다. 믿을 수 없는 목록 중에서 신뢰할 수 있으면서 간단하고, 빠른 방법이라 생각하기 때문이다. 하지만 여러 방안들을 조사했을 때, 어떤 상황에서는 사용할 수 있는 지, 또 어떤 지점이 문제가 되는 지 생각해볼 수 있었다. 그래서 고민했었던 여러 방안들을 소개하려고 한다. ### Remote IP 프록시가 없는 간단한 구조의 서비스라면 서버에 연결된 Remote IP 를 Client IP 로 추출할 수 있다. 하지만 Remote IP 가 실제 사용자의 IP 라는 확신이 없다면, Client IP 로 사용하기 어렵다. 사용자 네트워크 구성에서 proxy 가 있다면, 서버에 연결된 Remote IP 는 실제 사용자의 Client IP 가 아닐 수 있다. 여기에서 서비스 요구사항에 대한 명확한 정의가 필요해질 수 있다. 사용자 네트워크의 사설 IP 를 추출해야 하는 지, 아니면 공인 IP 를 추출해야 하는 지 정의가 필요하다. 사설 IP 는 서비스의 입장에서 큰 의미가 없기 때문에, 보통 공인 IP...