구조체?
영어로는 Struct라고 쓰이는데 말 그대로 어떤 구조를 가진 것이라고 보면 됩니다.
예를 들면 이런 것이죠.
type Person struct {
name string
age int
}
이런식으로 여러가지 변수들을 하나로 묶어서 하나의 이름으로 말하는게 구조체라고 보면 됩니다.
이것이 나온 이유는 프로그래밍이 발전된 과정을 보면 응집성(Cohesive)이 올라가고, 종속성(Dependency)이 낮아지는 방향으로 프로그래밍이 발전해 왔다고 보면 됩니다.
이렇게 만들어야 좋은 프로그래밍을 할 수 있기 때문에 이런 방향으로 계속 발전해 왔습니다.
그래서 이 Struct는 응집성을 높이는 방법입니다. 어떤 결합된 개념들이 있는데 예를 들면 성적 처리 프로그램을 만든다 했을 때 중요한 객체는 학생이 되고, 학생을 가지고 있는 정보들이 있고, 학생의 이름, 나이, 몇 반인지? 등등 이 학생을 포함하고 있는 여러 속성들이 있을 것입니다.
예를 들어 학생의 Struct를 만든다 하면
type Student struct {
name string
age int
class int
}
이런식으로 하나로 묶어서 하나의 변수로 관리 할 수 있게 만들어 주는게 Struct라고 보면 됩니다.
구조체를 선언하는 방법은 아까 썼듯이
type Person struct {
name string
age int
}
'Type'은 어떤 타입을 선언한다는 의미이고, 이 타입은 Person이라는 이름을 가지며 구조체를 가지있다고 선언한다는 의미이고, 이 구조체에는 name이라는 string속성이 있고, age라는 int속성이 있는데 이것들을 묶어서 Person이라는 하나의 구조체를 만들었고, 이거는 새로운 타입이라는 얘기입니다.
그래서 이 Person을 사용할 때는 main함수에서 사용하고 싶을 때 그냥 일반적인 변수 선언하듯이
func main() {
var p Person
이렇게 쓰거나
func main() {
p := Person {name:"김철수", 20}
선언 대입문을 써서 Person이란 타입을 만들고 이름은 "김철수", 나이는 20세로 넣을 수도 있습니다.
실제로 해봅시다.
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
var p Person
p1 := Person{"김철수", 15}
p2 := Person{name:"홍길동",age:21}
p3 := Person{name:"lala"}
p4 := Person{}
fmt. Println(p, p1, p2, p3, p4)
}
기본 형태에서 struct를 만들어 줍니다. 처음에 type이라고 적고, 그 다음에 struct이름을 Person이고, struct형태라고 선언해 줍니다.
거기 안에 이름, 나이가 있다고 넣어주고, main함수에 이 struct를 선언 하는 방법은 다음과 같습니다.
p1 변수처럼 추가를 해주어도 되고, p2변수 처럼 추가를 해주어도 되고, p3변수 처럼 하나만 하고 싶을 때 저렇게 해줍니다.
p3에서 나이를 대입을 안해주었기 때문에 초기값인 0이 될 것 입니다.
그리고 p4 처럼 아무것도 대입을 안해주면 이름에선 아무 문자열이 없는 초기값이 들어갈 것이고, 나이는 0이 들어가 있을 것입니다.
그래서 Println()으로 모두 출력 시켜 줍니다.
출력 값을 보면 P는 변수를 그냥 선언했고, 아무런 값을 주지 않았기 떄문에 초기값이 들어갔고, 마찬가지로 p1 ~ p4도 입력한대로 출력된 것을 확인 할 수 있습니다.
각각의 속성들을 가져오는 방법은 변수뒤에 '.'을 찍으면 그 안에 있는 속성들을 가져 올 수 있다. 이 방법으로 p를 수정해봅시다!
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
var p Person
p1 := Person{"김철수", 15}
p2 := Person{name: "홍길동", age: 21}
p3 := Person{name: "lala"}
p4 := Person{}
fmt.Println(p, p1, p2, p3, p4)
p.name = "Kevin"
p.age = 22
fmt.Println(p)
}
p의 이름과 나이를 변경해 주었습니다. 결과를 보죠.
정상적으로 p가 잘 바뀐것을 확인할 수 있습니다. 이게 Sturct의 기본적인 내용입니다.
아까도 말했지만 Sturct는 어떤 개념을 한 곳에 모아 놨다고 보면 됩니다. 이것들 대부분을 '객체'라고 보면 되고, 객체를 하나 만들었다고 보면 됩니다.
어떤 프로그램을 만들 때 ER다이어그램이라고 하는 그래프가 있는데 이것은 프로그램에 드러나는 객체들이 어떤 것들이 있는지 뽑고 그 각 객체들이 서로 어떤 상호작용을 하는지를 나타내는 형태의 그래프인데
이 그래프를 만들어 놓으면 프로그램 할 때 거기에 맞춰서 프로그램 하면 편합니다.
그래서 예를 들면 성적처리 프로그램을 만든다고 했을 때 객체를 뽑아보면 성적처리의 객체는 학생이 있을 것이고, 성적이 있을 것이고, 성적을 입력하는 사람인 선생님이라는 객체도 있을 것입니다.
이런 Entity를 뽑을 수 있고, 서로 간에 어떤 릴레이션이 있는지 간략하게 봅시다.
선생님 -------> 학생의 성적
(입력)
학생 --------> 자신의 성적
(조회)
우선 선생님이 성적을 입력 합니다. 그리고 학생은 자신의 성적을 조회 할 수 있습니다.
이런식으로 객체들 간에 릴레이션을 뽑고, 각 릴레이션을 정의 하는 다이어그램이 ER 다이어그램인데 이 프로그램을 말할 때 필요한 개체(Entity)들이 어떤 개체들이 필요하고, 이 각 개체들간에 어떤 상호작용을 하는지를 파악할 수 있고, 그것들을 구현하면 그 자체가 성적처리 프로그램이 됩니다.
그래서 이 객체들은 Struct로 표현 할 수 있는데 과거 C언어에서는 이 Struct가 말 그대로 구조만 가지고 있었습니다.
그러니까 학생인 Student라는 Struct가 있을 때 이 Student의 구조는 이름과 나이 같은 속성들만 가지고 있는데 현대 언어(Golang)에서는 Struct에다가 기능을 더했습니다.
이것을 FirstClass 라고 하는데 어떤 객체가 있을 때 이 객체가 단순히 속성만 가지고 있는게 아니라 기능(메소드)도 가지고 있는 걸 FirstClass 라고 합니다. 그래서 Go언어의 Struct는 FirstClass 입니다. 이 기능을 추가해보죠!
Person이라는 Struct가 있고, 기능을 추가 해볼 것인데 기능이라는 것은 다름이 아니라 function입니다.
그래서 이 속성에 해당하는 function을 추가할 수 있습니다.
package main
import "fmt"
type Person struct {
name string
age int
}
func (p Person) PrintName() { // 1
fmt.Print(p.name) // 2
}
func main() {
var p Person
p1 := Person{"김철수", 15}
p2 := Person{name: "홍길동", age: 21}
p3 := Person{name: "lala"}
p4 := Person{}
fmt.Println(p, p1, p2, p3, p4)
p.name = "Kevin"
p.age = 22
fmt.Println(p)
p.PrintName() // 3
}
1 : 함수 이름이 나오기 전에 괄호안에 어떤 타입이 가지고 있는 function인지를 나타내 줍니다. 그래서 Person이 가지고 있는 기능이라는 의미 입니다. 그리고 그 이름을 "PrintName"이라고 이름을 지어주고, 이 함수의 입력값은 없는 것으로 작성합니다.
2 : 그래서 이 함수의 역할은 p의 이름을 출력하는 함수가 됩니다. 그래서 Person이라는 객체에 이런 기능(메소드)을 추가 했다고 볼 수 있습니다.
3 : 이 함수를 사용하는 방법은 다음과 같습니다.
그 후 실행 시켜 보면 맨 마지막에 "Kevin"이 뜨는 것을 확인 할 수 있습니다.
이제 성적처리프로그램을 만들것인데, 아까 ER다이어그램을 그렸듯이
학생 --------> 자신의 성적
(조회)
학생이 자신의 성적을 조회 하는데 여기서 '조회'는 기능 부분이 됩니다. 각 Struct간에 어떤 릴레이션을 기능으로 볼 수 있는데 메소드로 만들 수 있습니다. 이렇게 만들어보죠!
package main
import "fmt"
type Student struct { // 1
name string
class int
grade Grade
}
type Grade struct { // 2
name string
grade string
}
func (s Student) ViewGrade() { // 3
fmt.Println(s.grade)
}
func main() { // 4
var s Student
s.name = "길동"
s.class = 1
s.grade.name = "과학"
s.grade.grade = "C"
s.ViewGrade()
}
1 : 학생 Struct 선언. 이름과 반, 자신의 성적을 가지고 있습니다.
2 : 점수 Struct 선언. 과목명, 과목 점수를 가지고 있습니다.
3 : 성적을 조회하는 함수.
4 : 다음과 같이 main함수에 각 형식에 맞게 데이터를 넣어 줍니다.
이제 출력시켜 봅시다!
입력한대로 출력 되는 것을 확인 할 수 있습니다.
다만 Student에서 알아야 할 것은 int, string은 Golang에서 자체로 지원하는 타입이고 Grade는 만든 타입이라는 점입니다.
그래서 Student는 grade라는 성적을 가지고 있고, ViewGrade이라는 메소드를 가지고 있습니다.
이 Struct는 이 외에도 기능이 굉장히 많습니다. Golang에서 대응되는 것이 C#, Java, C++에서는 class와 같은 것이라고 보면 됩니다.
이게 바로 객체지향 프로그램(OOP)의 가장 기본이 되는 개념이기 때문에 굉장히 중요합니다.
그리고 Golang에서 특이한게 메소드가 Struct안에서 정의 되는게 아니라 바깥에서 정의 되는 것이 다른 점입니다.
그리고 메소드라고 하지만 일반 함수와 같습니다. 그래서 어떻게도 표시 할 수 있냐면
func (s Student) ViewGrade() {
fmt.Println(s.grade)
}
func ViewGrade(s Student) {
fmt.Println(s.grade)
}
func main() {
s.ViewGrade()
ViewGrade(s)
}
이렇게도 표현할 수 있습니다. 위의 것은 어떤 객체에 속한 메소드이고, 아래 것은 개체에 속하지 않는 그냥 함수인데 기능은 똑같습니다. 호출하는 방법은 객체에 속한 메소드는 이렇게 s에 '.'을 찍고 호출해주고, 일반 함수는 입력값을 s로 집어넣으면 됩니다.
출력해보면 다음과 같습니다.
한가지 주의할 점이 있는데 가령 Student에 성적을 입력하는 기능이 있다고 봅시다.
package main
import "fmt"
type Student struct {
name string
class int
grade Grade
}
type Grade struct {
name string
grade string
}
func (s Student) ViewGrade() {
fmt.Println(s.grade)
}
func (s Student) InputGrade(name string, grade string) {
s.grade.name = name
s.grade.grade = grade
}
func ViewGrade(s Student) {
fmt.Println(s.grade)
}
func main() {
var s Student
s.name = "길동"
s.class = 1
s.grade.name = "과학"
s.grade.grade = "C"
s.ViewGrade()
ViewGrade(s)
s.InputGrade("수학", "A+")
s.ViewGrade()
}
이렇게 성적을 입력하는 함수를 만들고, s의 성적을 입력하고, s를 다시 출력시키면
분명히 "수학", "A+"로 입력을 했는데 출력 값은 "과학", "C"가 되었습니다.
이게 중요한 문제인데 Golang에서 함수 호출의 변수는 무조건 복사로 일어 납니다.
InputGrade()는 Student의 메소드이지만 그 안에 있는 함수의 입력값들이 모두 복사가 됩니다. 그래서 s Student도 복사가 되고, name string, grade string도 복사가 됩니다.
예를 들어서
func Add(x, y int) {
}
func main() {
a := 2
b := 3
Add(a,b)
}
Add라는 함수가 있고, 이 Add의 a,b를 호출하면 a라는 값이 복사되어 x는 2가 되고, y는 3이 되는데
main함수에 있는 a와 Add함수에 있는 x와 b와 y는 서로 다른 것입니다.
그렇기 때문에 여기서
package main
import "fmt"
type Student struct {
name string
class int
grade Grade
}
type Grade struct {
name string
grade string
}
func (s Student) ViewGrade() {
fmt.Println(s.grade)
}
func (s Student) InputGrade(name string, grade string) {
s.grade.name = name
s.grade.grade = grade
}
func InputGrade(s Student, name string, grade string) {
s.grade.name = name
s.grade.grade = grade
}
func ViewGrade(s Student) {
fmt.Println(s.grade)
}
func main() {
var s Student
s.name = "길동"
s.class = 1
s.grade.name = "과학"
s.grade.grade = "C"
s.ViewGrade()
ViewGrade(s)
s.InputGrade("수학", "A+")
s.ViewGrade()
}
s.InputGrade("수학", "A+")
이 부분의 s는 InputGrade 함수의(메소드지만) 입력값으로 처리가 되는 것입니다.
그래서 수학, A+은 InputGrade함수의 입력 값이된다. s, name, grade모두 입력값이 됩니다.
그런식으로 봤을 때 이 값들은 복사되어 넘어가기 때문에 s.InputGrade("수학", "A+")
의 s와 InputGrade(s student, name string, grade string)
의 s는 서로 다른 값입니다.
서로 메모리 변수를 가지고 있다고 보면 되고, 서로 값만 복사되서 같을 뿐이지 서로 다릅니다.
그래서 InputGrade(s Student, name string, grade string)
의 과목명과 성적을 바꾼다 하더라도 실제 s의 name이 바뀌지 않습니다. 그래서 저것들 변경시키기 위해서는 포인터가 필요합니다.
함수 호출 과정에서는 무조건 복사로 일어난다는 것, 복사가 일어났을 때 값이 전달 되는 것이지 그 메모리가 그대로 전달되는 것이 아니라는 것입니다.
그래서 이것들을 해결하기 위해서는 '포인터'가 나온 것입니다.
'프로그래밍(Basic) > Golang' 카테고리의 다른 글
[바미] Go - 숫자야구를 만들어보자! (1) (2) | 2020.12.22 |
---|---|
[바미] Go - 포인터에 대해 알아보자! (0) | 2020.12.22 |
[바미] TDD의 장단점에 대해 알아보자! (0) | 2020.12.21 |
[바미] TDD에 대해 알아 봅시다! (0) | 2020.12.21 |
[바미] Go언어 소개 (0) | 2020.12.16 |