안녕하세요! 지난 1월 19일 수요일에 진행한 10주차 스터디 내용 정리해보겠습니다!
이번에는 두 번째 프로젝트 챕터 단어 검색 프로그램과 객체지향 설계 원칙 SOLID에 대해 공부해봤습니다.
Ch26 단어 검색 프로그램
1. 해법
해법은 간단합니다. 파일 목록을 실행 인수로 넘겨주면 해당 파일을 하나씩 열어서 라인을 읽고 단어가 등장하면 결과에 추가하고 파일이 끝날 때까지 검사를 해서 모든 파일을 검사하면 결과를 출력하는 프로그램입니다.
2. 사전 지식
와일드카드
와일드카드를 사용해서 경로를 나타내면 여러 파일을 한 번에 불러올 수 있습니다.
os.Args 변수와 실행 인수
콘솔창에서 실행 명령을 수행할 때 실행 인수를 함께 넘겨주면 Args 슬라이스에 추가됩니다.
파일 핸들링
파일 열기 Open 함수 : name에 해당하는 파일을 읽기 전용으로 읽고 *File 타입인 파일 핸들 객체 반환
파일 목록 가져오기 Glob : 파일 경로를 넣어주면 경로에 해당하는 파일 리스트를 []string타입으로 반환
파일 내용 한 줄씩 읽기 : Scan(), Text() 메서드를 활용해서 한 줄씩 읽어올 수 있는 Scanner를 생성
단어 포함 여부 검사
strings 패키지의 Contatins 함수 : 읽어온 한 줄 내용 중에 찾으려는 단어가 있는지 검사
3. 파일 검색 프로그램 완성하기
앞선 사전 지식을 활용해서 파일 검색 프로그램 작성하면 아래와 같이 작성할 수 있습니다.
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
//찾은 라인 정보
type LineInfo struct {
lineNo int
line string
}
//파일 내 라인 정보
type FindInfo struct {
filename string
lines []LineInfo
}
func main() {
if len(os.Args) < 3 {
fmt.Println("2개 이상의 실행 인수가 필요합니다. ex) ex26.3 word filepath")
return
}
word := os.Args[1] //찾으려는 단어
files := os.Args[2:]
findInfos := []FindInfo{}
for _, path := range files {
//파일 찾기
findInfos = append(findInfos, FindWordInAllFiles(word, path)...)
}
for _, findInfo := range findInfos {
fmt.Println(findInfo.filename)
fmt.Println("--------------------------------")
for _, lineInfo := range findInfo.lines {
fmt.Println("\t", lineInfo.lineNo, "\t", lineInfo.line)
}
fmt.Println("--------------------------------")
fmt.Println()
}
}
- 단어가 포함된 한 줄 텍스트 정보를 포함하는 구조체 선언
- 실행 인수에서 찾으려는 단어와 파일명 가져오기
- FindWordInAllFiles() 함수 호출해서 파일 내 단어 찾기 → 반환 []FindInfo를 이용해서 탐색 결과 출력
func GetFileList(path string) ([]string, error) {
return filepath.Glob(path)
}
func FindWordInAllFiles(word, path string) []FindInfo {
findInfos := []FindInfo{}
filelist, err := GetFileList(path) //파일 리스트 가져오기
if err != nil {
fmt.Println("파일 경로가 잘못되었습니다. err:", err, "path:", path)
return findInfos
}
for _, filename := range filelist { //각 파일별로 검색
findInfos = append(findInfos, FindWordInFile(word, filename))
}
return findInfos
}
func FindWordInFile(word, filename string) FindInfo {
findInfo := FindInfo{filename, []LineInfo{}}
file, err := os.Open(filename)
if err != nil {
fmt.Println("파일을 찾을 수 없습니다. ", filename)
return findInfo
}
defer file.Close()
lineNo := 1
scanner := bufio.NewScanner(file) //스캐너 생성
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, word) { //한 줄씩 읽으면 단어 포함 여부 검색
findInfo.lines = append(findInfo.lines, LineInfo{lineNo, line})
}
lineNo++
}
return findInfo
}
- filepath의 Glob() 함수를 통해 path에 해당하는 파일 리스트 가져오기
- 파일별로 FindWordInFile() 함수 실행해서 파일 검사 (파일 열고 각 줄을 읽은 뒤 원하는 단어가 있는지 검사, 있을 경우 결과 리스트에 추가)
실행 결과의 일부입니다. 폴더에 있는 hamlet 텍스트 파일에서 brother라는 단어를 검색해 존재하는 라인을 저장해 출력하게 됩니다.
C:\Golang-Study\ch26\ex26.3>ex26.3 brother *.txt
hamlet.txt
--------------------------------
267 King. Though yet of Hamlet our dear brother's death
285 Or thinking by our late dear brother's death
291 To our most valiant brother. So much for him.
422 My father's brother, but no more like my father
4. 개선하기
위의 코드는 모든 파일 검색을 하나의 main() 고루틴에서 실행하지만 파일 개수가 늘어나면 검색에 오랜 시간이 듭니다. 이를 고루틴을 사용해서 파일 개수가 늘어도 빠르게 검색되도록 개선할 수 있습니다! 각 파일별로 작업을 할당하고 작업이 완료되면 채널을 이용해서 결과를 수집하는 방식 사용하면 속도와 성능면에서 개선된 코드를 작성할 수 있습니다.
Ch27 객체지향 설계 원칙 SOLID
아래 다섯가지 원칙은 Go언어에만 해당하는 것이 아니라 모든 객체 지향 언어, 심지어는 절차 지향 언어에도 적용되면 도움이 되니 이를 염두에 두고 보시면 좋을 것 같습니다!! :)
1. 객체지향 설계 5가지 원칙 SOLID
나쁜 설계
나쁜 설계란 경직성, 부서지기 쉬움, 부동성의 특징을 가진 설계입니다.
세 가지 특징을 정리해본다면 상호 결합도가 매우 높고 응집도가 낮다는 것입니다.
좋은 설계
그렇다면 좋은 설계는? 당연히 나쁜 설계 요소가 없는 설계를 뜻합니다. 즉, 상호 결합도가 낮고 응집도가 높은 설계가 좋은 설계입니다!
2. 단일 책임 원칙 SRP
정의 "모든 개게는 책임을 하나만 져야 한다."
이점 코드 재사용성을 높여준다.
위 코드의 FinanceReport는 회계보고서라는 책임과 보고서를 전송하는 책임을 가지며 단일 책임 원칙을 위배합니다. 이를 어떻게 개선하면 좋을까요?
위와 같이 Report라는 인터페이스와 ReportSender 구조체를 만들어서 회계 보고서라는 책임을 가진 구조체, SendReport 메서드로 각 책임을 분리하여 구현하면 단일 책임 원칙에 입각한 코드를 작성할 수 있습니다.
3. 개방-폐쇄 원칙 OCP
정의 "확장에는 열려 있고, 변경에는 닫혀 있다."
이점 상호 결합도를 줄여 새 기능을 추가할 떄 기존 구현을 변경하지 않아도 된다.
위 코드에서 전송 방식을 추가하려면 새로운 case를 만들어 추가해야 하는데 이는 기존 함수를 변경해야 하며 즉 개방-폐쇄 원칙을 위배합니다.
위처럼 메서드를 각 방식에 대해 따로 작성하면 새로운 전송 방식을 추가할 때 ReportSender를 구현한 새로운 객체만 추가하면 되어서 개방-폐쇄 원칙에 입각한 설계가 됩니다.
4. 리스코프 치환 원칙 LSP
정의 "q(x)를 타입 T의 객체 x에 대해 증명할 수 있는 속성이라 하자. 그렇다면 S가 T의 하위 타입이라면 q(y)는 타입 S의 객체 y에 대해 증명할 수 있어야 한다."
이점 예상치 못한 작동을 예방할 수 있다.
정의가 조금 어려운데 간추려 말하자면 하위 타입은 최소한 자신의 상위 타입에서 가능한 행위는 똑같이 수행할 수 있어야한다는 뜻입니다.
호출자 입장에서는 당연히 MarketingReport 인스턴스도 SendReport의 인수로 사용 가능할 것이라 예상하는데 실제로는 패닉이 발생한다고 합니다. 이렇게 하위 타입에 대해서도 마찬가지로 작동해야 하지만 그렇지 못하면 리스코프 치환 원칙에 위배됩니다.
5. 인터페이스 분리 원칙
정의 "클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다."
이점 인터페이스를 분리하면 불필요한 메서드들과 의존 관계가 끊어져 더 가볍게 인터페이스를 이용할 수 있다.
SendReport()는 Report 인터페이스가 포함한 4개의 메서드들 중 Report()만 사용하며 불필요한 메서드에 의존하게 됩니다.
많은 메서드들을 포함하는 커다란 인터페이스보다 적은 수의 메서드를 가진 인터페이스 여러 개가 더 낫다고 합니다! 인터페이스를 분리해서 의존 관계를 끊으면 더 가벼운 인터페이스를 설계할 수 있습니다.
6. 의존 관계 역전 원칙
정의 "상위 계층이 하위 게층에 의존하는 전통적인 의존 관계를 반전(역전)시킴으로써 상위 게층이 하위 게층의 구현으로부터 독립되게 할 수 있다."
원칙 1 "상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상 모듈에 의존해야 한다."
원칙 2 "추상 모듈은 구체화된 모듈에 의존해서는 안 된다. 구체화된 모듈은 추상 모듈에 의존해야 한다."
이점 구채화된 모듈이 아닌 추상 모듈에 의존함으로써 확장성이 증가한다. 상호 결합도가 낮아져서 다른 프로그램으로 이식성이 증가한다.
원칙 1 뜯어보기
기존에 탑다운 방식으로 많이 사고하게 되는데 탑다운 방식으로 설계를 한다면 네트워크 간 결합도가 높아지게 됩니다.
이렇게 추상 모듈에 의존하는 관계로 설게를 하면 각 모듈은 본연의 기능에 충실할 수 있고 서로 독립적이기 때문에 다른 어플리케이션에도 사용이 가능합니다. 즉 코드의 재사용성이 높아집니다.
원칙 2 뜯어보기
메일이라는 구체화된 모듈이 알람이라는 구체화된 모듈에 의존하는 에시인데요.
이처럼 추상모듈을 생성해서 그 어떤 모듈도 구체화된 모듈에 의존하지 않으면서 추상 모듈에 의존한다면 의존 관계 역전 원칙에 입각한 설계를 할 수 있게 됩니다. 또 예시의 EventListener, Event를 다양하게 추가할 수 있습니다! (이 경우 기존의 코드를 수정할 필요도 없으므로 개방-폐쇄 원칙도 지켜진다고 볼 수 있겠네요!)
7. 학습 마무리
SOLID 5가지 원칙들은 독립적인 개념이 아니라 서로 연결되어 있습니다. 한 가지만 지켜도 나머지가 지켜지는 경우가 많습니다. SOLID 원칙의 공통 목적은 좋은 설계, 결합도는 낮게, 응집도는 높개 설계를 하는 것입니다.
저는 이 챕터를 보면서 인터페이스의 중요성을 많이 깨달았고 무엇을 개발하던 설계부터 꼼꼼히 주의를 기울이면 전체적으로 발전된 코들르 작성하게 될 것이라고 생각했습니다. (물론 그게 쉽지 않겠지요...ㅠㅠ) Go 언어 뿐만 아니라 너무나 많이 사용하는 Java, 또 다른 객체 지향 언어들을 사용할 때에도 기억해두면 도움이 많이 될 것 같습니다!!
이렇게 챕터 26, 27을 정리해보았습니다~ 기존의 배운 내용과 새로운 지식으로 프로그램도 짜보고 설계에 있어서 중요한 SOLID 원칙도 짚어보았습니다!
Go 언어를 배우면서 이렇게 다른 언어 공부에도 도움이 되는 내용을 다루니 좋았습니다! 다음 주도 화이팅합시다!
'3-1기 스터디 > Golang' 카테고리의 다른 글
[9주차] 고루틴과 동시성 프로그래밍, 채널과 컨텍스트 (0) | 2022.01.20 |
---|---|
[6주차] 숫자 맞추기 게임 만들기, Go 언어의 슬라이스 (0) | 2021.12.01 |
[5주차] Chapter 15~16. 문자열, 패키지 (0) | 2021.11.30 |
[4주차] Go 언어의 배열, 구조체, 포인터 (0) | 2021.11.22 |
[3주차] Go언어의 if, switch, for (0) | 2021.11.11 |
댓글