일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- gin logger
- go
- golang gopher
- git
- gopath 환경변수
- go 패닉
- go air 환경변수
- go 마스코트
- 골랑 고퍼
- go clean architecture
- gin recovery
- go 맥 air 환경변수
- go panic
- go 맥
- go 맥 air
- air 환경변수
- go 캐릭터
- 신입개발자
- go air
- gin middleware
- 개발자
- go middleware
- 고루틴 채널
- go디자인패턴
- go 환경변수
- clean architecture middleware
- 좀비고루틴
- go 대기그룹
- go channel
- go recover
- Today
- Total
뽀미의 개발노트
Go 구조체와 인터페이스 본문
새로운 회사에서 백엔드 개발을 맡게 되었다. 원래는 C#으로 만들다가 이번에 Go로 바꾼다고 한다. 나는 애매한 중간 시기에 들어와서 고 공부하다가 바꿀 때쯤 투입될 예정인 듯!! 그래서 각 잡고 공부하고 있는데 Java 배웠을 때랑 많이많이 다른 언어라 너무 재밌고 무엇보다 캐릭터가 귀여워서(?) 맘에 든다. 곰돌이..? 수달..? 뭔진 모르겠지만 잘 지내보자 겸댕이!! (검색해보니 쥐라고 한다,,, 충격,,,,;;;;)
사수님께서 고 언어 특징 공부하라고 하셨는데 이왕 정리한 김에 블로그 써버리기~! 유후~!! 참고로 나의 대장님은 킹왕짱 멋진 개발자 이시다. 내가 원래 알고있고 좋아했던 서비스를 만드는 회사에 취업한 것도 기쁜데, 넘넘 좋은 대장님을 만나서 진짜 행복하다. GDG 연사자분으로 처음 뵀었는데 내가 초대박 간지폭발 대장님의 쫄다구가 될줄이야...!! 정말 좋다... 사수님께서 참고하라고 보내주시는 링크만 봐도 공부가 정말 많이 된다. 열심히 하고 싶어서 공부에 시간을 많이 쏟고 있다. 매일매일이 벅차게 행복하당. 대장님께 많이많이 배우고 싶고 앞으로가 넘넘 기대된당!!! 꺄하---!!! ♡⸜(ˆᗜˆ˵ )⸝♡
구조체(Struct)
Go에는 클래스가 없고 구조체(struct)가 있음.
Class (C#)
객체의 정의를 위해 사용됨. 참조 타입. 기본적으로 private으로 정의됨. 상속, 다형성 및 캡슐화 지원. 메모리를 힙에 할당. GC가 객체 생명 주기 관리하고 메모리 해제 자동으로 이루어짐. 동일한 객체를 여러 참조에서 공유 가능
상속을 통해 재사용성과 계층 구조 구현함. 다형성 쉽게 구현 가능. 메서드는 클래스 내부에 정의함. public, private, protected 등 접근 제한자를 지원하여 캡슐화 구현 가능.
생성자를 명시적으로 정의할 수 있음.
OOP를 중심으로 설계되어 복잡한 계층 구조의 동작을 쉽게 설계할 수 있음. GC 덕분에 메모리 관리를 신경쓸 필요는 없으나 런타임의 오버헤드 발생 가능함.
Struct (Go)
단순한 데이터 집합체. 값 타입이라 복사가 기본 동작(구조체를 변수에 대입하거나 함수 인자로 전달하면 새로운 복사본이 생성됨!!) 기본적으로 public으로 정의됨. 멤버 변수 및 함수가 외부에서 쉽게 접근 가능. 구조체를 직접 메모리에 저장하며, 필요시 포인터를 사용하여 참조로 전달 가능.
상속을 지원하지 않고, 대신 컴포지션(Composition) (구조체 안에 구조체 또는 인터페이스를 임베딩) 권장. 임베딩된 내부 타입의 메서드를 외부 타입이 마치 자신의 메서드인 것처럼 사용 가능. 다형성을 구현하려면 인터페이스 (구조체의 동작(메서드)를 정의하는 것) 사용해야함. 아래와 같이 메서드를 구조체 외부에서 정의하고 receiver를 통해 구조체와 연관시킴.
type Person struct {
Name string
}
func (p Person) Greet() {
fmt.Printf("Hello, my name is %s\n", p.Name)
}
접근 제한자는 없고 대문자로 시작하면 public, 소문자로 시작하면 private으로 쓰임.
생성자 메서드는 없고 구조체 리터럴이나 별도의 초기화 함수 만들어서 써야함.
DOP(Data-Oriented Design)에 적합함. (단순성과 성능에 중점을 둠). 구조체와 컴포지션을 통해 간단하고 효율적인 데이터 모델링 가능함. 값 타입으로 설계되어 성능과 안정성 제공.
왜 상속 대신 컴포지션을 사용했는지?
상속은 부모 객체의 행동 규약을 자식 객체가 위반하기 쉬움. 부모 클래스의 함수를 마음대로 오버라이딩 하면 예상치 못한 결과를 얻을 수 있음. 상속 관계가 복잡하게 얽혀있으면 어디가 잘못됐는지 찾기 어려움. 개체 간의 관계는 Is-A(차와 탈것)와 Has-A(차와 엔진)으로 나눌 수 있는데, Is-A의 경우 두 객체가 상속 관계를 맺고 있는 것으로, 너무 강하게 결합되어 있어 부모 객체의 일부가 변경되면 자식 객체에도 영향을 미침. 반면 Has-A의 경우 두 객체가 포함 관계를 맺고 있고 약하게 연결되어 있어 포함하는 객체의 변경이 포함되는 객체에 미치는 영향이 거의 없음! 위와 같은 상속 구조의 단점 때문에 점점 사라져야 한다고 말하는 개발자들도 있음.
컴포지션의 단점은 Class의 장점을 이용하지 못한다는 것(코드 구조가 장황해지고 반복이 심함)임. 컴포지션 + 인터페이스를 활용하면 다형성을 구현하면서도 구조체간의 상속 관계가 없으므로 유연성이 더 높음.
Go의 철학 : 구조체 + 인터페이스를 활용하여 코드 재사용성과 유연성을 동시에 확보하자!
인터페이스(Interface)
인터페이스 사용 이유
package main
import "fmt"
type Sender interface {
Send(address string, item string)
}
type FedexSender struct {
CompanyName string
}
func (f FedexSender) Send(address string, item string) {
fmt.Print(f.CompanyName + "에서 " + address + "로 " + item + "을(를) 발송했습니다\n")
}
type PostSender struct {
PostOffice string
}
func (p PostSender) Send(address string, item string) {
fmt.Print(p.PostOffice + "에서 " + address + "로 " + item + "을(를) 발송했습니다\n")
}
func SendBook(address string, item string, sender Sender) {
sender.Send(address, item)
}
func main() {
fedex := FedexSender{CompanyName: "Fedex"}
post := PostSender{PostOffice: "서울중앙우체국"}
SendBook("서울시 강남구", "노트북", fedex)
SendBook("부산시 해운대구", "책", post)
}
인터페이스 없이도 구조체에서 메서드 만들 수 있음. 그런데 왜 꼭 interface를 선언하고 그 안에 있는 함수를 구조체에서 구현하는 식으로 만드는 것일까? → 코드를 유연하고 편리하게 사용하기 위해서임! 서로 다른 구조체에 있는 함수 Send()를 main 함수에서는 하나로 합쳐 쓰고 싶음. 그래서 이 Send() 함수를 인터페이스 하나로 합쳐서 그 인터페이스 타입을 인자로 받는 SendBook() 함수로 묶음. 인터페이스도 타입이기 때문에 해당 타입으로 묶일수 있으면 어떤 구조체든 인자로 받을 수 있음. 그럼 SendBook() 함수 입장에서는 Sender가 누구든 상관없이 그냥 Send() 메서드만 구현했으면 다 받을 수 있음. 사용자도 SendBook이나 Send 메서드 내부를 알 필요 없이 그냥 호출만 하면 됨.
이처럼 내부 동작을 감춰서 서비스 제공자와 사용자 모두에게 자유를 주는 방식을 추상화(abstraction)라고 함. 서로가 서로에게 신경쓰지 않고 그냥 저 추상화된 관계(인터페이스) 중심으로 코딩할 수 있음. 이렇게 둘의 의존관계를 끊는 것을 디커플링(decoupling)이라고 함. 의존성은 낮을수록 좋음!!
덕타이핑(Duck Typing)
“만약 어떤 새를 봤는데 그 새가 오리처럼 걷고 오리처럼 날고 오리처럼 소리를 내면? → 나는 그 새를 오리라고 부를거야~!”
즉, 객체의 실제 타입이 아니라 그 객체가 무엇을 할 수 있는지(어떤 메서드를 제공하는지)에 따라 동작을 결정하는 방식이 덕타이핑임. 객체가 특정 인터페이스를 명시적으로 구현했다고 선언할 필요 없음.(마치 Java의 implements, C#의 : 처럼) 그냥 인터페이스의 메서드들을 전부 구현하고 있으면 해당 인터페이스로 간주됨!!
예시
package main
import "fmt"
// Speaker 인터페이스 정의
type Speaker interface {
Speak() string
}
// Dog 구조체
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
// Cat 구조체
type Cat struct{}
func (c Cat) Speak() string {
return "Meow!"
}
// Human 구조체
type Human struct{}
func (h Human) Speak() string {
return "Hello!"
}
func main() {
// Speaker 인터페이스 타입으로 다양한 객체를 담을 수 있음
var s Speaker
dog := Dog{}
cat := Cat{}
human := Human{}
// Dog가 Speaker로 사용
s = dog
fmt.Println(s.Speak()) // Output: Woof!
// Cat가 Speaker로 사용
s = cat
fmt.Println(s.Speak()) // Output: Meow!
// Human이 Speaker로 사용
s = human
fmt.Println(s.Speak()) // Output: Hello!
}
Dog가 Speaker 인터페이스를 ‘구현’한다고 명시적으로 표현하지 않아도, 인터페이스 내에 있는 모든 메서드(Speak())를 가지면 그냥 Speaker로 사용할 수 있음. Go는 클래스와 상속이 없지만 인터페이스와 덕타이핑을 활용해 다양한 타입이 하나의 공통된 동작을 수행하도록 할 수 있음.
덕타이핑의 장점
- 특정 타입에 얽매이지 않고 메서드를 기준으로 코드 작성 가능. 여러 타입의 객체를 하나의 인터페이스로 처리할 수 있음.
- 인터페이스 구현 여부를 타입 선언시 하는게 아니라 인터페이스가 사용될 때 결정함. 컴파일 시점에 인터페이스의 요구사항을 충족하는지(안에 있는 메서드를 다 구현했는지) 검증함. 그래서 런타임 에러를 줄일 수 있음.
- 새로운 구조체가 추가되어도 인터페이스의 메서드를 구현하기만 하면 기존 코드와 호환됨. 기존 코드 바꿀 필요 없음. 의존성 낮출 수 있음.
'Go lang' 카테고리의 다른 글
Go 프레임워크 + 테스트코드 라이브러리 (0) | 2024.12.20 |
---|---|
Go 고루틴과 채널 (1) | 2024.12.20 |
Go는 exception이 없다 (0) | 2024.12.20 |
Go 컴파일 언어 (0) | 2024.12.20 |
Go 포인터와 GC (0) | 2024.12.20 |