소프트웨어 디자인 패턴에서 싱글턴 패턴 (Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다. - wikipedia
싱글턴 패턴의 등장 배경에는 아래와 같은 이유가 있다.
유일한 인스턴스 유지: 일부 시스템에서는 특정 클래스의 인스턴스가 하나만 존재해야 할 때가 있습니다. 예를 들어, 시스템의 설정 정보를 담당하는 클래스, 로그를 관리하는 클래스, DB 연결을 관리하는 클래스 등이 있습니다. 이런 경우, 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 존재하도록 보장합니다.
전역 접근: 또한 싱글턴 패턴은 이 유일한 인스턴스에 대한 전역적인 접근 방법을 제공합니다. 즉, 어디서든 이 인스턴스를 참조할 수 있게 됩니다. 이는 애플리케이션 전반에서 공유해야 하는 데이터나 리소스를 관리하는 데 유용합니다.
제어된 리소스 공유: 한정된 리소스를 공유해야하는 경우에도 싱글턴 패턴이 유용합니다. 예를 들어, 데이터베이스 연결풀, 파일 시스템, 네트워크 소켓 등의 리소스는 한정되어 있기 때문에 효율적으로 관리하고 사용할 필요가 있습니다.
메모리 및 성능 최적화: 싱글턴 패턴을 사용하면 메모리 사용을 최적화하고 성능을 향상할 수 있습니다. 인스턴스를 한 번만 생성하므로 메모리 낭비를 줄이고, 계속해서 같은 인스턴스를 재사용함으로써 성능을 향상할 수 있습니다.
예전 Java 에서 Singleton 패턴에 관련해 정리된 내용이 있어 Go에서는 어떻게 활용하는가? 에 초점을 두어 작성하겠다.
- 싱크 패키지 에서 제공하는 구조체 중에 하나로 Once는 한 번만 수행되도록 도와주는 구조체이다. 제공되는 메서드 중 하나로 Do 메서드가 있으며 Do에서 실행되는 콜백 function 은 오직 한 번의 실행만을 수행한다.
type db struct {
/**
db 에 해당하는 값 이 저장
*/
}
var obj *db
var o sync.Once
func GetInstance() *db {
o.Do(func() {
runtime.Gosched()
obj = &db{}
fmt.Println("Crated DB Instance")
})
return obj
}
동일한 main func 을 수행한다면?
위와 같은 결과를 얻을 수 있다.
init 함수 활용 하기
- go에서는 패키지 import 간 최초 실행 해주는 함수가 있다. 바로 init() 함수인데 제일 좋아하는 방법이다. 바로 활용해 보자.
type db struct {
/**
db 에 해당하는 값 이 저장
*/
}
var obj *db
func init() {
obj = &db{}
fmt.Println("Db created")
}
func GetInstance() *db {
if obj == nil {
fmt.Println("Three is no db instance")
}
return obj
}
개인적으로는 init 함수를 선호한다.
이러한 싱글턴 디자인 패턴 에는 테스트의 어려움 이 있다. 변수를 아무래도 전역적으로 관리하다 보니 목킹 이 생각보다 어렵다.
이런 경우 인터페이스를 활용해서 추상화 작업을 통해 작성하는 것도 하나의 방법이 될 수 있다.
type MyDB interface {
DoSomething() // DB 작업을 위한 메서드
}
type db struct {
// db에 해당하는 값이 저장
}
var obj *db
func init() {
obj = &db{}
fmt.Println("DB created")
}
func (d *db) DoSomething() {
// DB 작업을 수행하는 메서드
}
func GetInstance() MyDB {
if obj == nil {
fmt.Println("There is no DB instance")
}
return obj
}
빌더 패턴이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴이다.
생성패턴 의 근간이 되는 기본 구조에 대해서 다시 한번 상기하고 넘어가자.
생성패턴 은 구현하는 구현체가 있고 구현체 의 추상화 클래스가 존재한다.
빌더패턴 은 다음 과 같은 문제점을 해결하고자 나왔다.
1. 하나의 객체에서 한 개 이상의 생성자 필드를 받게 된다면 , 생성함에 있어 깔끔하고 유지가능한 코드를 작성하는 것이 생각보다 어려울 수 있다.
2. 객체의 생성이후 필드값의 불변성이 요구될 때, 한 개 이상의 다양한 많은 속성을 불변으로 만드는 것은 생각보다 어려울 수 있다.
3. 객체의 복합 생성 로직은 메인 클래스 의 유지 및 확장성에 어려움을 야기할수 있다.
빌더 패턴 을 만들어 보자.
type House struct {
windows int
door int
floor string
rooms int
bathroom int
swimmingPoolSize int
swimmingPoolHeight int
gardenSize int
gardenFlower string
gardenTree string
garageSize int
garageDoor string
}
물론 작은 필드 들이지만 이것보다 많아질 수 있고 다양해질 수 있다고 가정해 보자. House를 생성하고자 한다면?
이제는 생각보다 많이 익숙해진 구조이고 , 다만 디렉터라는 구현체가 빌더의 총괄적인 핸들링을 담당하고 있다.
구체적인 클래스 설계도를 확인해 보자.
House 빌더라는 인터페이스가 있으며, 그거를 구현하는 각각의 빌더 구조체들을 구현하고 이 각각 의 구조체는 House라는 거대한
구조체를 상속받아서 사용할 것이다.
이렇게 생성된 HouseBuilder는 BuildDirector라는 구현체에 의해 핸들링되고 관리될 것이다.
바로 코드로 가보자.
인터페이스는 위와 같이 설정해 주었으며, 아래 있는 의문에 대해 생각해 보자. 최종 구현체에서 코드를 사용할 때 이유를 확인할 수 있다.
인터페이스의 구현을 위해 위와 같이 작성했으며, 특히나 go에서 ide를 사용하고 있다면,
var _ HouseBuilder = (*HouseNormalBuilder)(nil)
위 코드를 이용해 자동완성을 애용하자.
핵심이 되는 디렉터 구조체를 확인해 보자.
디렉터 구조체는 하우스 빌더를 생성간의 주입받게 된다. 이렇게 주입된 하우스 빌더를 통해 원하는 집을 디렉터가 정의한 순서에 맞춰
생산할 수 있다. 마치 프레임워크가 작동하듯이 말이다.
중간 함수를 확인하게 되면. 을 이용해 죄다 체이닝 해버렸다. 보다 직관적인 코드의 방식 구현을 위해 인터페이스에서 다음과 같이 정의를 했다. 본인 자신을 반환하게 된다면 위와 같은 체이닝 방식이 가능해진다.
그래서 클라이언트는 어떻게 이거를 사용할까?
빌더를 각각 주입해 주면서 우리는 대형 객체를 보다 손쉽게 선언하고 사용할 수 있다.
SwimmingPool House is {windows:40 door:10 floor:wooden rooms:0 bathroom:1 swimmingPoolSize:200 swimmingPoolHeight:180 gardenSize:0 gardenFlower: gardenTree: garageSize:0 garageDoor:}
Normal House is {windows:20 door:20 floor:wooden rooms:0 bathroom:3 swimmingPoolSize:0 swimmingPoolHeight:0 gardenSize:0 gardenFlower: gardenTree: garageSize:0 garageDoor:}
이렇게 코드가 작성되고 클라이언트가 사용한다고 가정할 때 빌더패턴 이 나오게 된 이유를 모두 충족하는가?
1. 하나의 객체에서 한 개 이상의 생성자 필드를 받게 된다면 , 생성함에 있어 깔끔하고 유지가능한 코드를 작성하는 것이 생각보다 어려울 수 있다.
- 각각의 빌더를 주입받아서 정해진 순서에 따라 하우스를 생산한다. 순서의 관리는 디렉터 가 담당하고, 각각의 구현체끼리는 느슨한 결합 형태를 가지게 된다.
2. 객체의 생성 이후 필드값의 불변성이 요구될 때, 한 개 이상의 다양한 많은 속성을 불변으로 만드는 것은 생각보다 어려울 수 있다.
- 하우스 구조체 의 필드값은 소문자로 임포트 되지 않는다. 즉 특정 메서드가 제공되지 않는다면? 불변하는 필드들이다.
3. 객체의 복합 생성 로직은 메인 클래스 의 유지 및 확장성에 어려움을 야기할 수 있다.
- 하우스 새로운 건축물 혹은 수영장 집의 업그레이드 함에 있어 어디를 수정하고 어디를 고쳐야 하는지 명확한가?
빌더패턴 의 특징을 다시 한번 알고 넘어가자.
- Builder Interface 가 모든 구현체에서 구현 함수를 정의한다.- Concrete Builder에서 Builder Interface를 구현하고 실제 로직을 캡슐화한다.- Director는 옵션적인 부분이며, Builder Interface의 함수를 호출해 Build의 대상을 제공한다.- Product에서는 Build pattern을 사용해 실질적인 인스턴스를 담당한다.
어느 정도 패턴을 공부하고, 자바 사람이라면 이 빌더패턴 은 너무나도 친숙한 패턴이 아닐 수 없다.Lombok 이 이 빌더패턴을 정말 잘 활용해서 객체의 생성을 도와준다. 고 와는 달리 자바에서는 필드의 생성자 값들을 하나씩만 넣으면서 초기화하기 위해서? 그에 해당하는 모든 생성자 클래스를 선언해주어야 한다. 창문 만 있는 생성자 클래스, 창문 욕조 가 있는 생성자 클래스 등등
갑자기 이 이야기를 왜 하냐고 의문이 들 수 있다.
House.builder().window().door().build();
롬복에서 는 builder라는 어노테이션을 제공해 주는데 이런 체이닝 방식을 이용해서 제공한다. 왜 이렇게 유명한 라이브러리(github star 11.9k)에서 우리가 지금 까지 배운 인터페이스, 함수 호출 외순서를 정하는 방식이 아닌 위와 같은 체이닝 방법을 활용해서 제공하고 빌더패턴을 적용했다고 할까? 우리가 배운 생성패턴의 기본 골격 구조는 인터페이스에 의존하는 구현체인데 방식이 많이 다르지 않은가?
이런 부분에 있어 어느 정도 유연성을 둔다고 한다. 그래서 글 초창기에 전통적인 빌더패턴이라는 표현을 사용했다.
빌더패턴의 목적 "유연하고, 효과적인 방법으로 객체 생성을 제공해야한다." 이다.
처음 제공된 코드처럼 마치 프레임워크 같이 작성하는 전통적인 빌더패턴 이 있고, 롬복 과 같이 다양한 라이브러리 에서는 사용자에게 보다 많은 유연성을 주기 위해 최근에는 이런 인터페이스 가 생략된 간단한 빌더패턴을 제공한다고 한다.
우리 는 파스타를 만드는 데 있어 이런 재료만 필요한 것 이 아닌 추가적인 재료가 필요하다. 마저 확인해 보자. 위와 동일한 구조를 가진 소스이다.
이렇게 동일한 구성을 가지고 Vegitables와 topping 도 선언해 주자.
이렇게 생성된 팩토리 들을 각각 의 방식으로 사용할 수도 있지만 보다 클라이언트의 특정 행동에 범위를 제한하기 위해 우리는 집합체를 생성하게 된다면? 클라이언트는 이 집합체를 얻음으로써, 각각의 팩토리를 가져와 객체의 구성을 만들수 있다는 장점이 있다. 아래 그림을 보자.
각각의 팩토리 를 조합하는 파스타 키트를 운용하는 것이다. 각각의 팩토리 인터페이스를 반환해 주는 방식이다 이렇게 했을 때 장점이 무엇인가?
파스타 키트는 다양한 팩토리를 주입받을 수 있으며 심지어 각각의 팩토리에서 또한 다양한 방식으로 팩토리 제공이 가능하다.
누들 팩토리에서 이탈리안 누들을 줄수도 있고, 한국 누들 을 줄수도 있는 등 팩토리를 갈아 끼울 수 있다는 장점 이 존재한다.
만약 원산지가 바뀌어 버렸다면? 그냥 팩토리 내부 구현체 만 추가적으로 작성해서 넣어주면 원산지가 바뀐 파스타 키트 를 클라이언트에서는 지속적으로 사용이 가능하다.
다시 말해 클라이언트는 파스타 키트 의 원산지 재료가 무엇이든 지 간에 궁금하지 않다. 제공되는 파스타 키트에서는 누들 소스 토핑 그리고 야채들이 제공되어야 하기 때문에 정말 코드 를 호출 하는 호출 부와 제공 하는 프로덕트 부 의 느슨한 결합이 완성된 형태라고 볼수 있다.
이렇게 느슨한 결합이 주는 장점으로는 테스트 코드의 작성 즉 파스타 키트 에서 생산되는 각각의 팩토리에 대해 테스트하기 쉬우며( 목 객체를 만들기 매우 편하다.), 변화되는 코드에 있어 손쉽게 교체 및 추가할 수 있다는 점이 있다.
그러면 장점만 있는가? 위의 단순한 코드 제공에만 무려 14개의 파일과 5개의 디렉터리로 나누어져 있다. 다시 말해 복잡하다. 이해하기 어려울 수 있으며 코드의 양이 정말 많아진다는 단점이 존재한다.
그래서 그러면 팩토리 메서드 패턴 이랑 추상 팩토리랑 도대체 무엇이 다른가 에 의한 의문점이 남을수 있다.
팩토리 메소드 패턴 은? 상속을 통한 객체 생성과 오직 하나의 제품을 생산하는 쪽에 키워드가 맞춰져 있고,
추상 팩토리는? 객체의 구성을 통해 필요 객체 생성을 만들어 낼 때 즉 제품의 구성 쪽에 키워드가 맞춰져 있다.
그렇다면 이런 다른 키워드를 포워딩하고 있는데 언제 사용해야 하는가?
제품군을 만들어야 한다면? 추상팩토리를
클라이언트 코드와 인스턴스의 생성 클래스를 분리시켜야 한다면? 팩토리 메서드 패턴을 활용하면 된다.
팀장 님 께서 EC2 서버 하나 구축해 보라고 하셨다. 기존에 있던 테스트 서버를 기준 삼아서 만들던 와중 RedHat으로 되어 있던 기존 꺼에서 aws linux 바꾸기로 해서 다시 만들려고 하던 와중 실수로 기존 테스트 서버를 터미네이트 시켜 버렸다...
이렇게 터미네이트 되어버린 인스턴스 는 기존에 가지고 있던 root ebs까지 모두 날려버리기 때문에 기존에 스냅샷 이 없다면.. 복구할 방법이 존재하지 않는다.... ㅠㅠ
설정이 완벽하게 끝난 서버라면 ebs 를 인스턴스 와 분리하던가 아니면 스냅샷 을 찍어 태그네임 박아서 기록해 두고, AMI까지 생성해서 관리한다면 보다 완벽하다. 꼭꼭 꼭 해두자.
어쨌든 구축하면서 했던 삽질 모든 걸 기록해보고자 한다.
EC2의 기본 세팅은 기존 ami를 사용하지 않고 aws에서 제공해 주는 이미지 템플릿을 사용했다.
ssh 키 같은거는 기존 pem 파일을 사용하기로 하였으며 docker를 이용해서 기존 디비들을 구축하려고 했다.
sudo yum update -y
sudo yum install -y docker
sudo service docker start
위 스탭 으로 기존 데이터를 업데이트해주고 도커 설치 후 서비스 시작해 주면 된다.
docker pull [mysql,redis,kafka]
docker run --name mysql -e MYSQL_ROOT_PASSWORD=plea2018 -p 3306:3306 -d mysql
docker run -d --name redis -p 6379:6379 redis docker run --name some-kafka -d -p 9092:9092 confluentinc/cp-kafka:latest
하다 보니 거지 같은 sudo 커맨드를 계속 써야 한다. sudo 코드를 없애기 위해 아래 명령어를 실행해 그룹에 추가해 주자.
sudo usermod -aG docker $USER
mysql부터 외부에서 접속 테스트를 실행하던 중 자꾸 kafka 도커가 상태가 계속 떨어진다... 로그를 확인해 보자.
KAFKA_ZOOKEEPER_CONNECT is required. Command [/usr/local/bin/dub ensure KAFKA_ZOOKEEPER_CONNECT] FAILED!
위와 같은 에러가 나오고 알아보니 zookeeper의 추가적인 설치와 설정이 필요하다. 왜?
Apache ZooKeeper는 Kafka가 클러스터 환경에서 효율적으로 작동하는 데 필요한 분산 조정 및 관리를 제공하므로 Apache Kafka의 중요한 구성 요소이다.
그래서 추가적인 zookeper 설치가 필요하다.
그전에 먼저 docker network create kafka-net을 이용해 카프카 용 별도의 네트워크를 설정해 준다.
이렇게 생성된 네트워크를 이용해 주키퍼 컨테이너 와 통신이 가능해진다. 그렇기에 네트워크 를 생성해서 주키퍼와 연결해 준다.
docker run -d --name zookeeper --network kafka-net -p 2181:2181 confluentinc/cp-zookeeper
실행해 주키퍼를 실행해 주면 역시나 바로 죽어버린다. 로그를 확인해 보면 Command [/usr/local/bin/dub ensure-atleast-one ZOOKEEPER_CLIENT_PORT ZOOKEEPER_SECURE_CLIENT_PORT] FAILED! 주키퍼 클라이언트 포트를 설정해 달라고 한다. 그냥 넣어주자. 한 번에 실행이 되는 적이 없다 아주
docker run -d --name zookeeper -p 2181:2181 -e ZOOKEEPER_CLIENT_PORT=2181 zookeeper:latest
주키퍼 실행 이 되었다면 이제 카프카를 켜주자.
docker run -d --name kafka --network kafka-net -p 9092:9092 e KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 confluentinc/cp-kafka 음 또 죽는다.
팀장님 이 말하시기를 주키퍼 내장된 컴포즈가 있을 텐데 왜 이렇게 어렵게 하냐라고 하신다.... ㅠㅠ 컴포즈 버전으로 다시 해보자.
저위에 주키퍼 다운로드하는 저 명령어 다필요 없이
docker run -d --name kafka johnnypark/kafka-zookeeper
이거만 실행 하면 그냥 돌아간다 ... ㅠㅠ
카프카 로그를 보면 이렇게 성공적으로 실행이 된다.
자 이제 팀장님의 추가적인 지시사항으로 이 서버는 오전 09 시에 켜지고 18시에 내려가는 서버이다 즉 이 서비스 들을 데몬에 등록해서 실행시켜주어 한다. 이런 팀장님의 의도를 모르고 aws lambda와 aws event bus , cloud formation이나 알아보고 있으니 그냥 하루를 아무것도 하지 않고 날렸다.
먼저 데몬에 대해 알아보자.
리눅스 시스템이 처음 가동될 때 실행되는 백그라운드 프로세스 일종으로 메모리에 상주하면서 특정 요청이 오면 즉시 대응할 수 있도록 대기 중인 프로세스이다. 바로 가보자.
서비스 작성
cd /etc/systemd/system , 으로 이동해서 원하는이름.service 로 작성해준다.
touch docker-init.service
vi docker-init.service
이렇게 적어주고 서비스 등록 을 해주자. 도커도 다운로드한 거기 때문에 이렇게 서비스 등록이 필요하다. systemctl start docker.service
systemctl start docker-init.service
위와 같이 작성하면 서버를 끄고 킬떄 마다 매번 명령어 실행 필요 없이 백그라운드에서 자동화 실행이 된다.
systemctl status docker
systemctl status docker-init
명령어를 실행해 등록이 되었는지 다시 한번 확인해 준다.
자 이제 시간에 맞춰 켜지고 꺼지는 자동화를 설정해 주자.
먼저 구조 가 어떻게 되는지 알아야 한다.
aws_event_bridge에서 특정시간에 이벤트를 발행시키는데 그 이벤트의 대상이 aws_lambda 함수가 된다. 그래서 우리는
람다 함수에 원하는 기능을 작성하고
이벤트 브리지 에는 이벤트 발행 규칙을 정해주면 된다.
aws_lambda를 생성해서 키고 끄는 것을 설정해 보자.
aws_lambda를 가서 생성한다. 노드 파이썬을 권장한다. 웹에서 수정이 가능하기 때문에 편하다.
이렇게 생성하면 자동으로 역할 이 생기게 되는데 iam 으로 가서 json 으로 역할 수정을 해준다.
최근 디자인 패턴을 공부하면서 Interface, Type에 대해 부족한 부분을 발견하고 이번 기회에 쉬는 날 정리해 보고자. 글을 작성한다.
GO TOUR 에서 작성된 글에 의하면?
인터페이스 타입 은 특정 함수를 정의 하는 용도로 사용한다. Java에 익숙한 사람들이라면 아래 예제는 당연하고 이해가 간다.
type Sender interface {
send()
}
type Bank struct {
account Account
}
func (b *Bank) send() {
fmt.Printf("%v 에서 돈을 보냅니다 .", b.account.ID)
}
type Account struct {
ID string
Balance uint
}
func main() {
var a Sender = &Bank{Account{"귀우 의 계좌", 123}}
a.send()
}
Sender 인터페이스 를 선언 (er을 이용해 Interface를 정의한다 "GO effecitve")
뱅크의 타입을 선언 했지만? Sender의 타입으로 받아서 샌더가 가지고 있는 함수를 사용할 수 있다.
당연하다 그렇다면 역으로도 가능한가 ? 저 특정 인터페이스에서 원하는 타입을 뽑아야 내하는 경우가 생긴다면 어떻게 할 것인가?
가능하다. 어떻게 ?
bank := &Bank{Account{"귀우 의 계좌", 123}}
var a Sender = bank
a.send()
if x, ok := a.(*Bank); ok {
x.send()
}
Interface로 선언된 타입은 최초 구현은 Bank의 포인터 타입이다.
추출하고자 하는 정확한 타입에 대해 위와 같이 추출해서 사용한다.
좋다 인터페이스 와 구현하는 구조체 간의 쌍방향 관계가 된다 는 사실을 알았으니 이제 타입 끼리 해보자.
동일한 인터페이스를 구현하는 2개 의 타입 간의 크로스 캐스팅이 가능한가?
type Email struct {
sender string
receiver string
address string
}
func (e *Email) send() {
fmt.Printf("%v 에서 %v 읭 %v 로 보냅니다. ", e.sender, e.receiver, e.address)
}
func main() {
var a Sender = email
if v, ok := a.(*Email); ok {
v.send()
}
if v, ok := a.(*Bank); ok {
v.send()
}
}
이 경우 콘솔에서는 메일에 해당하는 샌더를 실행시켜 준다. 즉 2번째 if 문이 false로 타입추출에 실패했다.
그래서 이게 뭐? 당연한 거 아닌가?
아래 예제를 보자.
type ExpressEmail struct {
*Email
price int
}
func main() {
//bank := &Bank{Account{"귀우 의 계좌", 123}}
email := &ExpressEmail{
&Email{"a", "b", "13472 Mortgatan6 Saltjobaden"},
10000000,
}
var a Sender = email
fmt.Println(reflect.TypeOf(a))
if v, ok := a.(*Email); ok {
v.send()
}
if v, ok := a.(*ExpressEmail); ok {
v.superFast()
v.send()
}
}
ExpressEmail로 인터페이스를 받아서 Email을 뽑으려고 하면 실패한다. 왜? ExpresEmail 구현체 안의 Email 도 하나의 고유한 타입이기 때문에 위와 같이 타입 추출이 불가능하다. (반면 자바는 가능하다.)
여기서 언어의 차이가 드러난다. 객체 지향과, 그렇지 않은 그래서 클래스의 상속에 따른 구조와 임베디드 타입에 의한 새로운 타입의 차이 가 있기 때문에 위와 같은 동일한 로직에서 서로 다른 결과를 얻게 된다.
반면 인터페이스 안에 인터페이스로 정의된다면 그건 타입캐스팅이 될까?
type Sender interface {
send()
}
type Receiver interface {
receive()
}
type SendReceive interface {
Sender
Receiver
}
type CommonSendReceive struct{}
func (c *CommonSendReceive) send() {
fmt.Println("Send Common Message")
}
func (c *CommonSendReceive) receive() {
fmt.Println("Receive Common Message")
}
func main() {
var sr SendReceive = &CommonSendReceive{}
if v, ok := sr.(Sender); ok {
v.send()
}
if v, ok := sr.(Receiver); ok {
v.receive()
}
}
놀랍게도 가능하다. 인터페이스 안에 임베디드 타입으로 구성된 인터페이스에 대해서는 타입 추출이 가능해진다.
왜 그럴까?
타입 추출은 컴파일러에게 인터페이스를 구현하는 것으로부터 내가 원하는 것처럼 뽑아달라고 부탁하는 것이다.
이렇게 되면 인터페이스 값에 기반하지 않은 타입이라면 에러가 발생하게 된다.
그러나 인터페이스 유니온 즉 인터페이스끼리 결합된 인터페이스에서는 분리가 가능해진다.
인터페이스의 결합을 코드로 풀면? 그냥 두 개의 메서드 합친 결과물이다. 그러니 당연히 필요한 부분의 분리가 가능해진다.
왜? 정확히는 컴파일러에게 내가 원하는 메서드를 어떤 걸 사용할 건데 그 타입으로 뽑아줘라고 부탁을 하는 것이기 때문이다.
아까 예제 ExpressEmail에서 email을 뽑아보자.
type Sender interface {
send()
}
type Email interface {
sendEmail()
}
type email struct{}
func (e *email) sendEmail() {
fmt.Println("고유 이메일")
}
type ExpressEmail struct {
Email
price int
}
func (e *ExpressEmail) send() {
fmt.Println("Send Express Email")
}
아까 와의 차이가 무엇인가? 인터페이스 타입을 인자로 가지냐 아니면 특정 타입이냐의 차이가 되고 그 결과는 타입 추출 의 가능 여부를 판별한다.
func main() {
var a Sender = &ExpressEmail{
&email{},
500,
}
if v, ok := a.(*ExpressEmail); ok {
v.send()
}
if v, ok := a.(Email); ok {
v.sendEmail()
}
}
위와 같은 코드는 전부 실행이 된다. 인터페이스의 강력함이 아닐수가 없다...
심지어 인터페이스가 아닌 구조체를 넣게 되더라도 그게 특정 인터페이스를 구현하고 있다면 인터페이스 의 타입추출이 가능해진다.
func main() {
var a Sender = &ExpressEmail{
email{},
500,
}
if v, ok := a.(*ExpressEmail); ok {
v.send()
}
if v, ok := a.(Email); ok {
v.sendEmail()
fmt.Println(reflect.TypeOf(v))
if e, ok := v.(*email); ok {
fmt.Println(reflect.TypeOf(e))
}
}
}
이 코드에서 보면 email이라는 구조체는 Email 인터페이스를 구현하고 있고 우리는 그 안에 email 있다는 사실을 알고 역으로 풀고자 한다.
그러나 마지막 if 문에서 프린트 라인이 찍히지 않는다. 왜?
reflect를 이용해 타입 추출을 하더라도 저 타입은 아직도 ExpressEmail이다. 즉 cpu는 이렇게 알고 있다는 소리이다.
아무리 우리가 밖에서 Email로 추론하고 값을 달라고 하더라도 cpu에서 지정된 오리지널 값에서 잘라서 준다는 의미이다.
아무래도 고에서 지원하는 상속과 embedded 타입 간의 간극을 잘 파악하여 사용하지 않는다면 오류를 남발하는 코드를 작성할 것이다.