서문에 "리팩토링은 개발 주기의 일부가 되어야 한다."라고 작성이 되어있다.

 

지난달부터 계속 개발하고 리팩토링을 하고 있는 현재 프로젝트를 보면 참 많이 공감되는 말이 아니지 싶다.

 

해당 파트는 구조체 구성이라는 큰 타이틀로 시작한다. 
"제니아"라는 시스템은 데이터 베이스를 가지고 있고,
"필러"라는 프런트엔드를 가진 웹서버는 해당 제니아를 이용하고 있고, 제니아의 데이터를 필러에 옮겨보는 작업이다.


글을 읽어가다보면 함수 설정과 파라미터의 이유가 기가 막히다.

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
	}
}

함수 파라미터를 포인터 타입으로 받는다. 굳이 포인터로 받지 않고 반환값으로 data를 던져 주어도 동일하게 동작한다. 

func (*Xenia) Pull() (data,error){}

위의 작성된 함수는 값을 반환하다. 이는 다시 말해 이스케이프 스택 즉 스택메모리에서 벗어나는 상황이 발생되고 이는 런타임 시점에 힙메모리에서 관리되어야 함을 의미한다. 

그러나 위에 작성된 기존 함수를 보면 d *Data를 이용해 메모리의 주소값만 받게 되고, 함수의 종료 이후 복사된 주소 d는 스택에서 사라진다.

매번 함수 호출마다 쓸모없는 이스케이프 스택을 방지하기 위해 위와 같이 함수를 사용한다.

 

약 8개월을 고언 어를 배우고 실제로 사용하면서 이러한 부분에 있어 고민을 해본 적이 없던 것 같다. 
아 팀원이 밑에 이렇게 사용했으니깐 이렇게 해야지. 오늘도 내가 만든 수십 개의 함수 중에 이러한 고민을 단 1이라도 해보았는가... 

해당 함수의 주석을 읽으면서 많은 생각이 든다. 


[구조체 임베디드]

type System struct {
	Xenia
	Pillar
}

 

두 개의 구조체를 system으로 구조체로 합쳤다. 이렇게 되면 System에서는 Xenia의 필드, Pillar의 필드를 모두 접근가능하다. 

[자주 사용했던 임베디드 방법]
해당 임베디드 구조체는 통상 gorm의 left join을 이용할 때 많이 사용한다. 

type User struct {
 id uint `gorm:"column:id;primaryKey"`
 name string
}

type UserWithLikeContent struct {
	User
	Content uint
}

요런 식으로 프로젝트에서 사용을 많이 했다. 이렇게 안 하고 select로 찍어오면 생각보다 코드가 더러워서 이렇게 구조체 임베디드를 활용하면 생각보다 좋다.

 

System이라는 구조체를 구현체가 아닌 인터페이스의 주입으로 변경하게 되면

type System struct {
	Puller
	Storer
}

type PullStorer interface {
	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
		}
	}
}

이렇게 인터페이스에 의존하게 변경할 수 있다. 

어디서 많이 보던 디자인 패턴 같다. (생성하면 추상팩토리가 될 테고, 두 가지 인터페이스를 합치니깐 퍼사드도 될 수 있고..  )

 

이렇게 구성된 시스템은 Puller, Storer 가 구현된 어떤 객체든 받을 수 있는 유연한 구조체가 되었다. 


이게 GO 가 디커플링 할 수 있는 방법이다. 인터페이스를 이용해 임베딩을 활용한다. 
당연히 인터페이스 만 활용해도 디커플링이 된다. 디커플링의 계층을 더 세부적으로 나누고 싶다면 위의 방법처럼 
임베딩을 활용하면된다.

 

위의 예제는 너무 명확하게 추상계층들이 나눠져 있어 보기 쉽지만. 실제 서비스에서는 생각보다 이렇게 나누는 게 쉽지 않다.

이번에 프로젝트 작성간 이렇게 추상화를 해야할 일이 생겼다.

repository 즉 데이터 접근 관련해서 mysql과 inmemoryDB의 추상을 각각 분리하고 싶었다.
인터페이스로 각 db 들을 추상화하고 해당 디비에 접근하는 서비스에 대해 추상화 하고 각 접근하는 방법들을 추상화했다.

위의 방식처럼 임베딩으로 묶고 구조를 작성하는데 생각보다 많은 시간이 걸렸다.

기존에 없던 추상 레이어를 만들어야 하기 때문에 생각보다 많은 코드를 작성해야 했기에 오히려 추상계층을 줄였다.
왜냐하면 하나의 서비스 떄문에 이렇게 추상화를 구성하는게 맞나 싶었다.

 

디자인 패턴을 배우면서도 패턴들의 단점에 대해서 항상 명확하게 숙지하고자 했다. 공통된 특징이 바로 지나친 추상화로 인한 코드 가독성 저하에 따른 안티패턴 이 되어가는 것 분명 주의해야 한다.

 

디커플링은 분명 좋은 방식의 코드작성 이다. 확장성이 정말 좋다. 다만 처음부터 할 필요는 없는 것이다.

처음부터 앞으로 이렇게 확장될 테니 팩토리메서드를 적용한 생성자 함수를 만들자 등과 같이 필요 없는 작업은 과감하게 버리면 된다.

글의 첫 시작과 동일하게 "리팩토링 은 코드 작성 주기의 한 사이클이 되어야 한다." 다시 한번 강조하고 싶다.

출처 : https://github.com/hoanhan101/ultimate-go

 

GitHub - hoanhan101/ultimate-go: The Ultimate Go Study Guide

The Ultimate Go Study Guide. Contribute to hoanhan101/ultimate-go development by creating an account on GitHub.

github.com

 

'Go > Go Basic' 카테고리의 다른 글

AES 암호화 적용 In Go  (1) 2023.10.10
Ultimate-Go-06 [에러처리]  (0) 2023.09.19
Ultimate-Go-03  (2) 2023.09.07
Ulitmate-Go-02  (0) 2023.09.05
Ulitmate-Go-01 (string,메모리 패딩)  (3) 2023.08.19

+ Recent posts