프로그래밍/golang
ultimate in go(7)- decoupling
by 나도한강뷰
2023. 1. 24.
디커플링
- Xenia 라는 시스템은 데이테베이스를 가지고 있다. Pillar라는 또 다른 시스템은 프론트엔드를 가진 웹서버이며 Xenia를 이용한다. Pillar 역시 데이터베이스가 있다. Xenia의 데이터를 Pillar에 옮겨보자.
- 포인트는 2가지
- 테스트 커버리지가 어느정도인지
- 추후 변경되는 부분들을 리팩토링으로 대응가능한지
- 각 계층이 자신의 역할을 제대로 한다면, 성능이나 최적화는 나중에 해도 된다.
- 옳바른 동작에 초점을 맞추는게 중요
stucture composition(구조체를 통한 기능 구현)
- xenia의 데이터를 추출하는 기능이 필요(pull)
- 추출한 데이터를 pillar에 저장하는 기능 필요(store)
- pull과 store를 유기적으로 묶어주는 기능
- 실제 데이터가 아닌 임의로 "Data"라고 정의해주겠다.
package main
import (
"errors"
"fmt"
"io"
"math/rand"
"time"
)
//random초기화 과정(이 부분은 구현하지않은 데이터베이스에서 이미 읽은것, 읽지않은것 등을 처리하기위해서 임시 구현해놓은부분)
func init() {
rand.Seed(time.Now().UnixNano())
}
type Data struct {
Line string
}
type Xenia struct {
Host string
Timeout time.Duration
}
// xenia의 데이터를 추출하는 함수(case에따라서 이미 읽은것, 에러가 나는경우, 정상작동하는 경우를 케이스를 만들어서 리팩토링을 염두한것)
func (*Xenia) Pull(d *Data) error {
switch rand.Intn(10) {
case 1, 9:
return io.EOF
case 5:
return errors.New("Error reading data from Xenia")
default:
d.Line = "Data"
fmt.Println("In:", d.Line)
return nil
}
}
type Pillar struct {
Host string
Timeout time.Duration
}
//d로 넘어온 데이터를 저장하는 과정
func (*Pillar) Store(d *Data) error {
fmt.Println("Out:", d.Line)
return nil
}
//임베딩을 통한 내외부 구조이용
type System struct {
Xenia
Pillar
}
// pull과 store의 확장성을 제공하는 함수
func pull(x *Xenia, data []Data) (int, error) {
for i := range data {
if err := x.Pull(&data[i]); err != nil {
return i, err
}
}
return len(data), nil
}
func store(p *Pillar, data []Data) (int, error) {
for i := range data {
if err := p.Store(&data[i]); err != nil {
return i, err
}
}
return len(data), nil
}
// xenia에서 가져온 값을 pillar에 저장하는 함수
func Copy(sys *System, batch int) error {
data := make([]Data, batch)
for {
i, err := pull(&sys.Xenia, data)
if i > 0 {
if _, err := store(&sys.Pillar, data[:i]); err != nil {
return err
}
}
if err != nil {
return err
}
}
}
func main() {
sys := System {
Xenia: Xenia {
Host: "localhost:8000",
Timeout: time.Second,
},
Pillar: Pillar {
Host: "localhost:9000",
Timeout: time.Second,
},
}
if err := Copy(&sys, 3); err != io.EOF {
fmt.Println(err)
}
}
- 각각의 structure에 필요한 기능을 메소드로 만들고, 기능을 가진 structure를 system에 임베딩시킨다.
- system을 정의해주고, 그 system을 이용하여서 copy를 실행해준다.
인터페이스로 디커플링하기
- pull은 구체화되어 있고, 그것은 xenia에서만 사용한다. 그것을 추상화 시킨다.
- store도 마찬가지
- puller, storer
- xenia와 pillar를 넘어가면서 작동하던것을 puller, storer를 넘어가면서 작동하는것으로 추상화시킨다.
package main
import (
"errors"
"fmt"
"io"
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type Data struct {
Line string
}
type Puller interface {
Pull(d *Data) error
}
type Storer interface {
Store(d *Data) error
}
type Xenia struct {
Host string
Timeout time.Duration
}
func (*Xenia) Pull(d *Data) error {
switch rand.Intn(10) {
case 1, 9:
return io.EOF
case 5:
return errors.New("Error reading data from Xenia")
default:
d.Line = "Data"
fmt.Println("In:", d.Line)
return nil
}
}
type Pillar struct {
Host string
Timeout time.Duration
}
func (*Pillar) Store(d *Data) error {
fmt.Println("Out:", d.Line)
return nil
}
func pull(p Puller, data []Data) (int, error) {
for i := range data {
if err := p.Pull(&data[i]); err != nil {
return i, err
}
}
return len(data), nil
}
func store(s Storer, data []Data) (int, error) {
for i := range data {
if err := s.Store(&data[i]); err != nil {
return i, err
}
}
return len(data), nil
}
type System struct {
Xenia
Pillar
}
func Copy(sys *System, batch int) error {
data := make([]Data, batch)
for {
i, err := pull(&sys.Xenia, data)
if i > 0 {
if _, err := store(&sys.Pillar, data[:i]); err != nil {
return err
}
}
if err != nil {
return err
}
}
}
func main() {
sys := System{
Xenia: Xenia{
Host: "localhost:8000",
Timeout: time.Second,
},
Pillar: Pillar{
Host: "localhost:9000",
Timeout: time.Second,
},
}
if err := Copy(&sys, 3); err != io.EOF {
fmt.Println(err)
}
}
- pull과 store함수의 변수를 각각의 구조체의 포인터를 받는게 아니라, interface로 받는다.
- 그렇게 되면, pull store메소드를 가진 모든 대상에 대해서 input으로 받을 수 있기때문에
- structure와 직접적인 상관관계는 떨어지게 된다.
인터페이스 구성 사용하기(interface composition)
- 코드는 필요한 부분만 기술하겠다.
package main
import (
"errors"
"fmt"
"io"
"math/rand"
"time"
)
type PullStorer interface {
Puller
Storer
}
type System struct {
Xenia
Pillar
}
func Copy(ps PullStorer, batch int) error {
data := make([]Data, batch)
for {
i, err := pull(ps, data)
if i > 0 {
if _, err := store(ps, data[:i]); err != nil {
return err
}
}
if err != nil {
return err
}
}
}
func main() {
sys := System {
Xenia: Xenia {
Host: "localhost:8000",
Timeout: time.Second,
},
Pillar: Pillar {
Host: "localhost:9000",
Timeout: time.Second,
},
}
if err := Copy(&sys, 3); err != io.EOF {
fmt.Println(err)
}
}
- pullStorer를 통해서 2가지의 인터페이스를 조합하였다.
- 이 interface를 쓰기위해서는 2가지 method가 구현되어 있어야된다.
- system일 경우 2가지 method가 임베딩 되어있기때문에 ps인터페이스에 들어갈 수 있다.
- 이전 copy의 경우 system type을 사용하기때문에 추후 오해의 가능성이 있다.
- ps값에 sys의 pointer값이 들어가게 되고, 그 system은 2개의 임베딩을 통해서 각각의 structure의 매소드를 참조하게 된다.
- 결국 system은 pull store두개의 매소드를 참조하고 있으므로 ps에 정상적으로 사용 될 수 있게된다.
인터페이스 구성을 이용한 디커플링
- system의 구체타입을 xenia pillar를 쓰는 대신에, 인터페이스 puller, storer를 사용한다.
- 이것은 xenia, pillar를 필요로 하는대신에, pull, store을 가지고 있는 어떤 애들이든 주입할 수 있게 변경된다.
- system은 structre를 기반으로 작동하는게 아닌, 행위를 가지고있는 인터페이스를 기반으로 작동하기때문에 하나의 디커플링 계층을 갖게된다.
- 변한 부분만 기술하겠다.
type System struct {
Puller
Storer
}
func Copy(ps PullStorer, batch int) error {
data := make([]Data, batch)
for {
i, err := pull(ps, data)
if i > 0 {
if _, err := store(ps, data[:i]); err != nil {
return err
}
}
if err != nil {
return err
}
}
}
func main() {
sys := System {
Puller: &Xenia {
Host: "localhost:8000",
Timeout: time.Second,
},
Storer: &Pillar {
Host: "localhost:9000",
Timeout: time.Second,
},
}
if err := Copy(&sys, 3); err != io.EOF {
fmt.Println(err)
}
}
- 이렇게되면 sys를 선언할때, 그 구성요소는 인터페이스를 만족하는 Xenia, Pillar의 주소값이 된다.
- 이러한 값은 인터페이스를 통해서 받고있기때문에 매소드가 있는 어떤것이든지 사용할 수 있게된다.