기본 콘텐츠로 건너뛰기

빌더 패턴을 사용한 객체의 생성


빌더 패턴은 객체를 생성할 때 쓰이는 유용한 패턴입니다. 하지만 빌더에 대한 추가적인 구현이 필요해서 인지, 생각보다 많은 사람들이 빌더가 필요한 상황에서도 복잡한 생성자를 사용해서 객체를 생성합니다. 그리고 그에 대한 댓가로, 개발자의 파라미터 입력 실수에 의한 오류를 접합니다.

이 글에서는 Java 환경에서 생성자와 점층적 생성자, 그리고 빌더에 대해서 알아봅니다. 그리고 계층적 클래스 구조에서 빌더를 사용하는 방법에 대해서 설명하고자 합니다.

## 객체의 생성

빌더에 대해서 이야기 하기 전에, 먼저 객체를 생성하는 여러가지 방법에 대해서 알아보겠습니다.

### 생성자

가장 많이 사용하는 방법은 생성자를 통해 객체를 생성하는 것입니다. 생성자는 정의되어 있는 class 를 기반으로 객체를 생성하는 기본적인 방법이다. `new Class(…)` 형태로 직관적이고, class 내에 정의만 하면 되기 때문에 군더더기 없습니다.

하지만 객체가 가지고 있는 속성이 많을 경우에는 이야기가 좀 달라집니다. java 의 생성자는 입력 받는 파라미터를 순서와 파라미터의 클래스(Class) 또는 원시 타입(primitive type)으로 구분합니다. 이 때 사용자는 다음과 같이 객체를 생성해야 합니다.

```
new Something(id, weight, height, message, pages, rate);
```

이렇게 되었을 때, `weight` 와 `height` 의 순서가 변경된다면 어떤 일이 발생할까요? 아마 `Something` 객체는 개발자가 의도한 것과는 정반대로 생성되어 프로그램의 동작에 문제가 발생할 것입니다. 실제로 개발하면서 이런 문제를 많이 겪습니다. 이런 문제는 변수명과 순서를 매핑할 수 없는 언어적인 한계에서 발생합니다. (python 에서는 명명된 선택적 매개변수를 지원하기 때문에 이런 문제가 없습니다.)

### 점층적 생성자

일반적인 생성자를 사용할 때 파라미터가 길어지는 문제점을 방지하기 위해서 점층적 생성자를 사용할 수 있습니다. 생성자에서 필요한 파라미터만 받아서, 기본값을 할당할 수 있는 방법입니다.

하지만 이럴 경우에도 모든 파라미터를 사용하여 초기화해야하는 경우에는 위 문제가 해결되지 않습니다. 또 생성자가 많아질 수록 코드를 수정하기 어려워집니다.

```
class Something {
	// ...
	public Something(int id, int weight, int height) {
			this(id, weight, height, "", 1, 0.0D);
		}
	}

	public Something(int id, int weight, int height, String message, int pages, double rate) {
		this.id = id;
		this.weight = weight;
		this.height = height;
		this.message = message;
		this.pages = pages;
		this.rate = rate;
	}
}

// 필요한 항목만 사용하여 생성할 수 있도록 점층적 생성자를 선언해줄 수 있다. 
new Something(id, weight, height);

// 하지만 결국에 많이 필요한 경우에는 같은 문제가 유지된다. 
new Something(id, weight, height, message, pages, rate);
```

## 빌더

빌더 패턴은 생성자와 마찬가지로 어떤 객체를 생성해주는 역할을 합니다. 아래 빌더 코드를 살펴보겠습니다.

```
public class Coffee {
	private int shot;
	private String type;

	protected Coffee(Builder builder) { // -- (A)
		this.shot = builder.shot;
		this.type = builder.type;
	}

	public static class Builder { // -- (B)
		private int shot;

		public Builder() {
		}

		public Builder shot(int shot) { // -- (C)
			this.shot = shot;
			return this;
		}

		public Builder type(String type) {
			this.type = type;
			return this;
		}

		public Coffee build() { // -- (D)
			return new Coffee(this);
		}
	}
}
```

-   (A) 에서 빌더를 통해 각 필드가 설정되고 객체가 생성될 수 있도록 생성자를 구현합니다.

-   (B) 는 Builder 클래스의 선언부입니다.
    
    -   외부에서 Builder 를 생성할 수 있어야 하기 때문에 `public` 이어야 합니다.
    
    -   객체마다 생성될 필요가 없기 때문에 `static` 하게 정의하였습니다.

-   (C) 은 빌더에서 각 속성의 설정자입니다.
    -   Builder 를 반환하기 때문에, 메서드 체이닝 형태로 필요한 파라미터를 한번에 설정할 수 있습니다.

-   (D) 는 최종적으로 객체를 생성하여 반환할 수 있도록 하는 `build()` 메서드입니다.

위 처럼 빌더 클래스를 정의해주었다면, 아래와 같이 빌더를 통해 객체를 생성할 수 있습니다. 생성자를 통한 생성과 비교해보겠습니다.

```
Coffee coffee = new Coffee.Builder().shot(3).type("HOT").build();
```

생성자처럼 한번에 객체를 만들어내는 것이 아니라, 필요한 속성들을 하나씩 모아서 개발자가 필요한 시점에 `build()` 를 호출하여 객체를 만들어줄 수 있습니다. 이렇게 빌더를 만들어 사용을 하면 다음과 같은 장점이 있습니다.

### 장점

-   Java 에서는 지원되지 않는 명명된 선택적 매개변수(named optional parameter) 를 흉내낼 수 있다.

-   객체를 일관성있게 유지할 수 있도록 돕는다. (consistent)

-   매개변수의 입력 시점을 다르게 정하면서도, 객체의 불변성을 유지할 수 있다.

-   계층적인 클래스에 대해서 사용하기 편리하다.

### Java 에서는 지원되지 않는 명명된 선택적 매개변수를 흉내낼 수 있다.

Java 는 생성자나 메서드에 파라미터를 전달할 때, 이름을 지정할 수 없습니다. 순서에 의해서만 해당하는 파라미터가 무엇인지 결정됩니다. 이 때문에 앞서 생성자에서 발생했던 문제가 드러납니다.

하지만 빌더를 사용하면 이런 문제를 어느 정도 우회할 수 있습니다. 파라미터를 설정할 때 명확한 설정자를 통하기 때문에, 어떤 파라미터에 무엇을 넣는 지 개발자가 인지할 수 있습니다.

```
# python 이라면 다음과 같은 방법을 통해 혼란을 줄일 수 있다.
coffee = Coffee(shot=3, type="HOT")
```

```
// 자바는 명명된 선택적 매개변수를 지원하지 않지만,
// 빌더 패턴을 활용하여 명명된 선택적 매개변수의 장점을 취할 수 있다. 
Coffee coffee = new Coffee.Builder().shot(3).type("HOT").build();
```

### 객체를 일관성있게 유지할 수 있도록 돕는다.

어떤 객체가 있는데, 그 객체를 사용할 때 무결한 상태임을 유지하는 것은 중요합니다.

객체를 사용할 때마다 각 필드에 대해서 검증을 해주어야한다면, 이는 유지 보수 비용을 크게 만들 뿐만 아니라 개발할 때에도 성가시고, 어떤 위험이 있을 지 모르기 때문에 개발하기를 두렵게 만듭니다. 하지만 생성되는 순간에 무결성이 검증되고, 객체에 대한 수정이 되지 않음이 보장된다면 마음이 한결 편안해집니다.

빌더는 각각의 필드에 대해서 검증 로직을 만들어둘 수 있고, 빌드 될 때 한번 더 검증해줄 수 있기 때문에 객체의 일관성을 유지할 수 있도록 돕습니다. 더 나아가서 클래스에서 생성자의 접근자를 private 하게 만들고, 클래스의 설정자(setter)를 제거하면 객체를 불변하게(immutable) 만들 수 있습니다.

```
// a 는 필드를 설정하는 코드의 사이에서 사용될 수 있기 때문에, 무결한지 확인해주어야 한다.
Coffee a = new Coffee()
a.setShot(...)
// ... setType(..) 가 호출되지 않았을 때, 
// ... 이 사이에 a 가 사용된다면 이는 정상적인 객체인걸까 아닌걸까?
a.setType(...) 
```

```
// b 는 생성되었을 때, 필요한 필드를 모두 가지고 있을 것이기 때문에,
// 생성된 객체는 무결함이 보장될 수 있다. (무결성에 대한 검증 코드는 build() 나 각 필드 설정자에 포함되어야 한다. )
Coffee b = new Coffee.Builder()
	.shot(...)
	.type(...)
  .build();
```

### 매개변수의 입력 시점을 다르게 정할 수 있다.

필드 마다 필요한 매개변수가 준비되는 시점이 다를 수 있습니다. 이 때 생성자라면, 매개변수가 준비된 시점과 이를 설정하는 코드가 떨어져 가독성을 떨어뜨릴 수 있습니다. 아래 코드를 살펴보겠습니다.

```
// blahblah 
// blahblah 
// (1) blahblah for value `Type`
String type = createType(...);

// blahblah 
// blahblah 
// (2) blahblah for value `shot`
int shot = createShot(...);

// blahblah 
// blahblah 
// blahblah 

// (3) Coffee 의 각 매개변수가 만들어진 시점과 사용하는 시점이 떨어져 있으면
//     가독성이 떨어지고, 유지보수하기 어려워 진다. 
Coffee a = new Coffee(shot, type);
```

(1) 과 (2) 에서 준비된 `message` 와 `count` 가 (3) 에 와서야 사용됩니다. 명명된 변수가 실제 사용되는 부분과 떨어져있게 되면 코드의 가독성이 떨어지고, 실제 사용되는 시점을 추적해야되기 때문에 유지보수가 어려워집니다.

하지만 빌더를 사용하면, 준비가 된 시점에서 빌더의 필드에 저장해둘 수 있어 가독성을 높일 수 있습니다. 아래 코드를 살펴보겠습니다.

```
Coffee.Builder builder = new Coffee.Builder();

// blahblah 
// blahblah 
// (1) blahblah for value `message`
builder.type(createType(...)); // 매개변수가 만들어진 시점에서 바로 활용된다.

// blahblah 
// blahblah 
// (2) blahblah for value `count`
builder.shot(createShot(...));

// blahblah 
// blahblah 
// blahblah 

// (3) 생성하는 시점에 만들어둔 매개변수는 이미 builder 에 설정되어 있기 때문에 가독성이 좋다.
Coffee b = builder.build(); 
```

(1) 과 (2) 를 보면, 변수는 따로 명명할 필요없이 `builder` 의 설정자에 바로 전달되어 사용됩니다. 그리고 (3) 에서 생성하는 시점에 만들어졌던 값들은 모두 `builder` 에 포함되어있기 때문에, `build` 호출만 하면 바로 생성될 수 있습니다.

생성자를 사용했을 때보다 가독성과 유지보수면에서 훨씬 간단해집니다. 실제 개발하는 경우에는 생각보다 사이에 들어있는 코드의 길이가 길어지는 경우가 발생하기 때문에, 빌더를 사용하여 가독성을 높일 수 있습니다.

### 계층적인 클래스에 대해서 사용하기 편리하다.

빌더는 뿐만 아니라, 계층적인 구조를 가진 클래스에서도 쉽게 사용할 수 있습니다. 부모 클래스의 빌더를 상속하는 구조로, 자식 클래스에서는 자식 클래스에 추가된 필드에 대해서만 설정하는 빌더를 만들 수 있습니다.

아래 부모 클래스의 빌더 코드를 따라가보겠습니다.

```
public class Coffee {
	private Type type;

	protected Coffee(Builder> builder) { // -- (1)
		this.type = builder.type;
		this.shot = builder.shot;
	}

	// ...

	public static abstract class Builder> { // -- (2)
		private Type type;

		public Builder() {
		}

		public T type(Type type) { // -- (3)
			this.type = type;
			return self();
		}

		// ...

		protected abstract T self(); // -- (4)

		public abstract Coffee build(); // -- (5)
	}
}
```

-   (1) 은 `Builder` 를 통해 `Coffee` 를 생성할 수 있도록 만들어주는 생성자입니다.
    -   우리는 이 생성자를 코드 외부에서 호출하고 싶지 않지만, 상속 관계에서는 호출하여 부모 클래스의 생성자를 재사용해야 하므로 `protected` 여야 합니다.
    -   자식 클래스 빌더도 전달될 수 있도록 하기 위해 `Builder` 정의와 같게 정의하였습니다. 이는 (2) 에서 더 자세하게 살펴보겠습니다.
    -   생성자 내부에서는 `builder` 에 설정된 각 필드를 통해 객체 필드를 설정하고 생성합니다.
-   (2) 는 빌더의 내부 클래스 선언부 입니다.
    -   하위 클래스에서 명확하게 정의해주어야 하는 추상 메서드가 존재하기 때문에 `abstract class` 로 정의하였습니다.
    -   받을 타입 파라미터를 `T extends Builder` 로 정의하였습니다.
        -   `Builder` 를 상속하는 `T` 를 타입 파라미터로 받습니다.
        -   이렇게 정의하면 `T` 에 `Builder` 속성을 부여하여, 자식 클래스 빌더에서 부모 클래스 빌더에 정의된 값들을 사용할 수 있도록 합니다.
        -   [Java 의 제너릭에 대해 익숙하지 않다면, 다음 글을 참고해주세요.](https://medium.com/@joongwon/java-java%EC%9D%98-generics-604b562530b3)

-   (3) 은 빌더에서 필드의 설정자입니다.
    -   `self()` 호출하여 자식 클래스에서 명확하게 정의될 타입 파라미터 `T` 를 반환합니다. `self()` 에 대한 자세한 설명은 (4) 에서 살펴보겠습니다.
    -   결국 `self()` 를 통해 빌더 자신을 반환하고, 반환된 빌더를 통해 메서드 체이닝으로 나머지 필드를 추가로 설정할 수 있게 됩니다.
-   (4) 는 `this` 로 부모 클래스 타입을 반환하지 않고, 자식 클래스를 반환할 수 있도록 하기 위한 추상 메서드입니다.
    -   `this` 가 아닌 `self()` 를 사용하는 이유는, `this` 를 사용하면 부모 클래스 타입으로 반환되기 때문에, 자식 클래스에서 항상 형변환을 하여 사용해야 합니다.
    -   [자바에서는 self 타입이 없기 때문에 이런 우회 방법을 사용하는 것인데, 이런 패턴을 simulate self-type idiom 이라고 합니다.](https://stackoverflow.com/questions/7354740/is-there-a-way-to-refer-to-the-current-type-with-a-type-variable)
-   (5) 는 최종적으로 `Coffee` 객체를 생성하는 빌드 메서드입니다. 자식 클래스에서 이를 구현하여 적절한 객체를 생성하여 반환합니다.
부모 클래스에서 빌더를 정의하였다면, 이제 실제로 원하는 객체를 만들기 위해서 자식 클래스 빌더에서 부모 클래스 빌더를 상속해주어야 합니다. 아래 자식 클래스의 빌더 코드를 따라가보겠습니다.

```
public class AmericanoCoffee extends Coffee {
	private int water;

	protected AmericanoCoffee(Builder builder) { // -- (6)
		super(builder);
		this.water = builder.water;
	}

	public static class Builder extends Coffee.Builder { // -- (7)
		private int water;

		public Builder() {
			super();
		}

		@Override
		protected Builder self() { // -- (8)
			return this;
		}

		@Override
		public Coffee build() { // -- (9)
			return new AmericanoCoffee(this);
		}

		public Builder water(int water) { // -- (10)
			this.water = water;
			return self();
		}
	}
}
```

-   (6) 은 자식 클래스의 생성자입니다.
    -   (1) 에서 선언한 부모 생성자를 재사용하여, 자식 클래스에서 추가된 필드만 설정합니다.
    -   자식 클래스 빌더를 파라미터로 받습니다. (1) 에서 `Builder>` 를 타입으로 정했기 때문에, 자식 클래스 빌더 타입 그대로 전달하더라도 컴파일 에러가 발생하지 않습니다.
-   (7) 에서 `Coffee.Builder` 를 상속합니다.
    -   `Coffee.Builder` 에 자식 클래스 빌더인 `Builder` 를 타입 파라미터 `T` 로 전달해주었기 때문에, 나머지 `T`로 반환해주었던 메서드들을 `Builder` 로 반환해주어야 합니다.
-   (8) 에서 (4) 를 구체 메서드로 구현합니다.
    -   여기에서 자기 자신 `this` 를 반환해줌으로써, 타입 형변환 없이 코드를 사용할 수 있습니다.
    -   `self()` 가 아니었다면, 부모 클래스의 설정자를 사용할 때마다 타입 형변환이 필요했을 것 입니다.
-   (9) 는 실제 자식 클래스의 객체인 `AmericanoCoffee` 를 생성하여 반환합니다.
-   (10) 은 자식 클래스에서 추가된 필드를 설정하는 메서드 입니다.

이렇게 계층화된 상속 구조에서도 빌더 패턴을 잘 활용할 수 있습니다.

### 단점

하지만 빌더 패턴에도 단점이 없는 것은 아닙니다. 아래와 같은 점을 생각할 수 있습니다.

-   객체를 생성하기 위해 빌더가 먼저 만들어져야합니다.
    -   빌더가 만들어지기 위한 생성 비용이 큰 것은 아니지만, 성능이 중요한 어플리케이션이라면 충분히 단점으로 작용할 수 있습니다.
-   빌더를 구현하기 위한 노력이 필요합니다.
    -   위에서 살펴보았듯이, 생성자를 구현하는 것보다 빌더에 대한 별도 class 정의가 필요합니다. 명확하게 필드가 증가하는 방향으로 변경되지 않는다던지, 생성 시 파라미터의 양이 적은 경우에는 빌더를 선언하는 개발 비용이 더 클 수 있기 때문에 이를 고려해보아야합니다.

## 정리

빌더 패턴은 생성자를 사용할 때 발생할 수 있는 여러가지 문제점을 방지할 수 있도록 도와줍니다.

-   Java 에서 지원되지 않는 명명된 선택적 파라미터의 장점을 가질 수 있습니다.
-   객체를 일관성있게 유지할 수 있도록 돕습니다.
-   매개변수의 입력 시점을 다르게 정하면서도, 객체의 불변성을 유지할 수 있도록 도와줍니다.
-   계층적인 클래스에 대해서 사용하기 편리합니다.

자바의 생성자를 통해서 생성하면 위에서 설명한 문제들이 있었다면, 빌더 패턴을 사용하여 그런 단점들을 개선해나가보는 것도 좋을 것 같습니다. [계층적 클래스에서의 빌더 패턴 구현 예제는 업로드해두었으므로 참고해주세요.](https://github.com/le0park/java-builder-example)

### 참고
-   [계층적 클래스 빌더 패턴 예제 코드](https://github.com/le0park/java-builder-example)
-   [이펙티브 자바3/E, p14, p105](http://www.yes24.com/Product/Goods/65551284)
-   [**Is there a way to refer to the current type with a type variable?**](https://stackoverflow.com/questions/7354740/is-there-a-way-to-refer-to-the-current-type-with-a-type-variable)

댓글

이 블로그의 인기 게시물

안드로이드 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...