본문 바로가기
프로그래밍/golang

ultimate in go(7)- decoupling

by 나도한강뷰 2023. 1. 24.

디커플링

  • Xenia 라는 시스템은 데이테베이스를 가지고 있다. Pillar라는 또 다른 시스템은 프론트엔드를 가진 웹서버이며 Xenia를 이용한다. Pillar 역시 데이터베이스가 있다. Xenia의 데이터를 Pillar에 옮겨보자.
  • 포인트는 2가지
    • 테스트 커버리지가 어느정도인지
    • 추후 변경되는 부분들을 리팩토링으로 대응가능한지
  • 각 계층이 자신의 역할을 제대로 한다면, 성능이나 최적화는 나중에 해도 된다.
  • 옳바른 동작에 초점을 맞추는게 중요

    stucture composition(구조체를 통한 기능 구현)

    1. xenia의 데이터를 추출하는 기능이 필요(pull)
    1. 추출한 데이터를 pillar에 저장하는 기능 필요(store)
    1. 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의 주소값이 된다.
  • 이러한 값은 인터페이스를 통해서 받고있기때문에 매소드가 있는 어떤것이든지 사용할 수 있게된다.