6장 데이터 보호
의문점
학습목표
- ‘getter와 setter를 사용하지 말것’ 으로 캡슐화 강제하기란?
- ‘getter와 setter 제거하기’ 로 getter제거하기란?
- ‘공통 접사를 사용하지말것’을 위한 ‘데이터 캡슐화’ 사용이란?
- ‘순서 강제화’로 불변속성제거란?
키워드
6.1 getter없이 캡슐화 하기
정의
- Boolean 이 아닌 필드에 getter나 setter 사용하지 말것
- 즉 직접할당하거나 반환하는 메서드를 의미함
- getter 존재시
- 캡슐화를 해제하고 불변속성을 전역적으로 만듬
- 객체 반환 후 반환받은 곳은 객체를 다른곳어 더 전달이 가능하고 이것은 제어 불가능
- setter 존재시
- setter수정시 시그니처 유지할 수있는 또 다른 간접적 레이어 도입할 수있음
- 실제로는 setter를 통한 새로운 데이터 구조를 반환하도록 getter를 수정하는 것임
- 밀결합(tight coupling)의 한 형태임
- 가변(mutable) 객체에서만 발생하는 문제임
- 필드를 비공개하는것의 장점
- 푸시기반(push-based)의 아키텍처 장려함
- 데이터에 가깝에 연산을 이관함
- 풀기반(pull-based)의 아케틱처
- 데이터를 가져와 중앙에서 연산함
- 푸시기반(push-based)의 아키텍처 장려함
- 풀기반 아키텍처
- 수동적인 데이터 클래스 & 소수의 관리자 클래스로 이어짐
- 데이터와 관리자 사이, 데이터 클래스 간 밀결합을 가져옴
- 푸시기반 아키텍처
- 인자로 데이터를 전달함
- 효용에 따라 분산이 됨
스멜
- ‘낯선 사람’에게 말하지말라 요약하는 디미터 법칙에서 유래함
- 여기서 낯선사람이란 직접접근은 못하지만 ‘참조얻을 수 있는 객체임’
- getter를 통해 발생함
의도
- ‘참조얻을 수 있는 객체임’ 와 상호작용시 문제점(pull-based)은 객체 가져오는 방식과 밀결합함
- 객체 소유자의 내부 구조에 대해 어느정도 알고 있어야함
- push-bsased 아케틱처는 서비스와 같은 메서드를 노출함 → 메서드 사용자는 메서드 내부구조 신경쓰지 안아도 됨
// 1. 비공개 설정 및, 클로스로 코드이관 class KeyConfiguration { constructor(private color: string, private _1: boolean, private removeStrategy: RemoveStrategy) { } getColor() { return this.color; } is1() { return this._1; } //삭제 -> 더이상 사용되지 않으므로 // private getRemoveStrategy() { return this.removeStrategy; } removeLock(){ //새로운 메소드 remove(this.removeStrategy); } } class Key implements Tile { constructor(private keyConf: KeyConfiguration) { } isAir() { return false; } isLock1() { return false; } isLock2() { return false; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = this.keyConf.getColor(); g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } moveHorizontal(dx: number) { // remove(this.keyConf.getRemoveStrategy()); this.keyConf.removeLock(); //이관 moveToTile(playerx + dx, playery); } moveVertical(dy: number) { // remove(this.keyConf.getRemoveStrategy()); this.keyConf.removeLock(); //이관 moveToTile(playerx, playery + dy); } update(x: number, y: number) { } }
- ‘참조얻을 수 있는 객체임’ 와 상호작용시 문제점(pull-based)은 객체 가져오는 방식과 밀결합함
6.1 리팩터링 패턴 : getter 와 setter 제거하기
설명
- 이 패턴으로 기능을 데이터에 더 가깝게 이동하여 getter, setter 제거가능
절차
- getter, setter 사용되는 모든곳에서 오류 발생하도록 private 로 설정
- 클래스로의 코드 이관으로 오류수정
- 클래스로의 코드 이관의 일부로 인라인화 함 → 기존것 삭제
실습
// // getter와 setter제거 초기코드 // class Website { // constructor (private url: string){} // getUrl() { return this. url;} // } // class User { // constructor (private username: string) {} // getusername() {return this.username}; // } // class BlogPost { // constructor (private author: User, private id: string) {} // getId() { return this.id; } // getAuthor () { return this. author; } // } // function generatePostLink(website: Website, post: BlogPost) { // let url = website.getUrl(); // let user = post.getAuthor(); // let name = user.getUsername(); // let postId = post.getId(); // return url + name + postId; // } // 1. private로 변경 / 클래스로 코드 이관으로 오류수정 class Website { constructor (private url: string){} getUrl() { return this. url;} } class User { constructor (private username: string) {} getusername() {return this.username}; } class BlogPost { constructor (private author: User, private id: string) {} getId() { return this.id; } // private getAuthor() { return this.author; } -> 사용하지 않으므로 삭제 getAuthorName(){ return this.author.getUsername(); //push 형으로 username을 바로 받음 }; } function generatePostLink(website: Website, post: BlogPost) { let url = website.getUrl(); // let user = post.getAuthor(); // pull형으로 접근할 필요가 없어짐 let name = user.getUsername(); let postId = post.getId(); return url + name + postId; }
6.2 규칙 : 공통접사를 사용하지 말것 (간단한 데이터 캡슐화 하기)
정의
- 코드에는 ‘공통 접두사, 접미사’ 있는 메서드나 변수가 없어야함
- 메서드, 변수에 접미사 접두사 붙이는 경우 있음 → 컨텍스트를 잘 전달함 → 요소간 긴밀성 나타냄
- 방법은 클래스 사용으로 메서드,변수 그룹화 장점은 외부 인터페이스를 완전히 제어할 수있음
- 데이터를 숨김으로서 불변속성이 클래스 내에서 관리되게 할 수있음 → 유지보수성 향상
스멜
- 단일책임원칙이라고 함
의도
- 단일책임으로 클래스 설계하려면 원칙과 개요가 필요함 → 이 규칙은 하위책임 식별에 도움이 됨
규칙 적용하기
- player 클래스 옮기기
참고 (getter, setter 제거)
// 전 class Air implements Tile { isAir() { return true; } isLock1() { return false; } isLock2() { return false; } draw(g: CanvasRenderingContext2D, x: number, y: number) { } moveHorizontal(player: Player, dx: number) { moveToTile(player, player.getX() + dx, player.getY()); } moveVertical(player: Player, dy: number) { moveToTile(player, player.getX(), player.getY() + dy); } update(x: number, y: number) { } getBlockOnTopState() { return new Falling(); } } class Flux implements Tile { isAir() { return false; } isLock1() { return false; } isLock2() { return false; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#ccffcc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } moveHorizontal(player: Player, dx: number) { moveToTile(player, player.getX() + dx, player.getY()); } moveVertical(player: Player, dy: number) { moveToTile(player, player.getX(), player.getY() + dy); } update(x: number, y: number) { } getBlockOnTopState() { return new Resting(); } } class Player { private x = 1; private y = 1; getX() { return this.x; } getY() { return this.y; } setX(x: number) { this.x = x; } setY(y: number) { this.y = y; } draw(g: CanvasRenderingContext2D) { g.fillStyle = "#ff0000"; g.fillRect(player.getX() * TILE_SIZE, player.getY() * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } // 후 class Air implements Tile { isAir() { return true; } isLock1() { return false; } isLock2() { return false; } draw(g: CanvasRenderingContext2D, x: number, y: number) { } moveHorizontal(player: Player, dx: number) { player.move(dx, 0); } moveVertical(player: Player, dy: number) { player.move(0, dy); } update(x: number, y: number) { } getBlockOnTopState() { return new Falling(); } } class Flux implements Tile { isAir() { return false; } isLock1() { return false; } isLock2() { return false; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#ccffcc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } moveHorizontal(player: Player, dx: number) { player.move(dx, 0); } moveVertical(player: Player, dy: number) { player.move(0, dy); } update(x: number, y: number) { } getBlockOnTopState() { return new Resting(); } } class Player { private x = 1; private y = 1; draw(g: CanvasRenderingContext2D) { g.fillStyle = "#ff0000"; g.fillRect(this.x * TILE_SIZE, this.y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } moveHorizontal(dx: number) { map[this.y][this.x + dx].moveHorizontal(this, dx); } moveVertical(dy: number) { map[this.y + dy][this.x].moveVertical(this, dy); } move(dx: number, dy: number) { moveToTile(this, this.x + dx, this.y + dy); } pushHorizontal(tile: Tile, dx: number) { if (map[this.y][this.x + dx + dx].isAir() && !map[this.y + 1][this.x + dx].isAir()) { map[this.y][this.x + dx + dx] = tile; moveToTile(this, this.x + dx, this.y); } } moveToTile(newx: number, newy: number) { map[this.y][this.x] = new Air(); map[newy][newx] = new PlayerTile(); this.x = newx; this.y = newy; } }
6.3 리팩터링 패턴 : 데이터 캡슐화
설명
- 접근할 수있는 지점을 제한하고, 구조를 명확하게 함
- 이름을 단순하게, 응집력을 명확히
- 이 경우 더 많은 수의 작은 클래스가 도입됨 → 유익함 → 소극적이지 말자
- 중요 이점은 변수를 캡슐화 하는데있음
- 범위 제한 시 클래스 내의 메서드만 데이터를 수정할 수있음 → 메서드
절차
- 클래스를 만듭니다.
- 변수를 새로운 클래스로 이동하고 let을 private으로 바꿔니다. 변수의 이름을 단순한 것으로 정하고 변수에 대한 getter와 setter를 만듭니다.
- 변수가 더 이상 전역 범위에 없기 때문에 컴파일러가 오류를 발생시켜 모든 참조를 찾을 수 있게 해줍니다. 다음의 다섯 절차를 통해 이 오류들을 수정합니다.
- 새 클래스의 인스턴스에 적합한 변수 이름을 선택합니다.
- 접근을 가상의 변수에 대한 getter 또는 setter로 바꿉니다.
- 2개 이상의 다른 메서드에서 오류가 발생한 경우 이전의 변수명을 가진 매개변수를 첫 번째 매개변수로 추가하고, 동일한 변수를 첫 번째 인자로 호출하는 쪽에 넣습니다.
- 한 메서드에서만 오류가 발생할 때까지 반복합니다.
- 변수를 캡슐화했다면 변수가 선언된 지점에서 새로운 클래스를 인스턴스화합니다. 그렇지 않으면 오류가 발생한 메서드에 인스턴스화하는 부분을 만듭니다.
// // 초기코드 // let counter = 0; // function incrementCunter(){ // counter++; // } // function main(){ // for(let i = 0 ; i < 20 ; i++){ // incrementCunter(); // console.log(counter); // } // } // //1단계 , 2단계 // function main(){ // for(let i = 0 ; i < 20 ; i++){ // incrementCunter(); // console.log(counter); // } // } // class Counter{ // private counter = 0; // getcounter() {return = 0;} // setcounter(c : number){ // this.counter = c; // } // } // 3단계 class Counter{ private counter = 0; getCounter() {return 0;} setCounter(c : number){ this.counter = c; } } function incrementCunter(counter : Counter){ counter.setCounter( counter.getCounter() + 1 //setter로 대체 ) } function main(){ let counter = new Counter(); for(let i = 0 ; i < 20 ; i++){ incrementCunter(counter); //인자로 전달되는 가장변수 (이후 만들어줌) console.log(counter.getCounter()); } }
6.3 복잡한 데이터 캡슐화
- 데이터 캡슐화 → 메서드 처리 위해 ‘클래스로의 코드이관’ → 메서드의 인라인화 → getter, setter 제거하기
6.4 리팩터링 패턴 : 순서 강제화 (순서에 존재하는 불변속성 제거하기)
class Map { private map: Tile[][]; transform() { this.map = new Array(rawMap.length); for (let y = 0; y < rawMap.length; y++) { this.map[y] = new Array(rawMap[y].length); for (let x = 0; x < rawMap[y].length; x++) { this.map[y][x] = transformTile(rawMap[y][x]); } } } window.onload = () => { map.transform(); gameLoop(map); }
다른 메서드보다 먼저 map.transform() 을 호출해야하는 불변속성 제거 효과 있음
- 순수 불변속성(sequence invariant)라고 함 → 순서강제화
설명
- 가장 바람직한 리팩터링은 컴파일러에게 프로그램이 어떻게 실행되는지 가르쳐주고 → 원하는대로 실행되는지 확인이 가능함
- OOP에서 생성자가 객체 메서드 보다 먼저 호출되어야함 →특정 순서로 발생하게 할 수있음
- 강제하려는 실행절차별로 하나의 클래스를 도입은 간단하게 가능함
//초기 코드 // function deposit(to: string, amount: number) // { // let accountId = database.find(to); // database.updateOne( // accountId, // { $inc: {balance: amount }}); // } //1단계 : 마지막 실행 메서드에 캡술화 적용 // class Transfer{ // deposit(to: string, amount: number) // { // let accountId = database.find(to); // database.updateOne( // accountId, // { $inc: {balance: amount }}); // } // } //2단계 : 생성자에 첫 실행 메서드 호출 // class Transfer{ // constructor(from: string, amount:number){ // this.deposit(from, -amount); // } // deposit(to: string, amount: number){ // let accountId = database.find(to); // database.updateOne( // accountId, // { $inc: {balance: amount }}); // } // } //3단계 : 개선 class Transfer{ constructor(from: string, private amount:number){ this.depositHelper(from, -this.amount); } deposit(to: string){ //돈 그냥 사라질수있음 주의!! -> 다른클래스로 감추는것 추천 this.depositHelper(to, this.amount); } //도우미 메서드 private depositHelper(to: string, amount: number){ let accountId = database.find(to); database.updateOne(accountId, {$inc: {balance: amount }}); } }
6.5 열거형을 제거하는 또 다른 방법
- 비공개 생성자를 통한 열거
- 열거형에 대한 메서드 지원하지 않는 경우 private 생성자를 사용해서 이를 우회할수 있는 기법 존재
- 모든 객체는 생성자를 호출해서 생성해야함 → 생성자를 private로 만들면 클래스 내부에서만 객체 생성 가능
- 숫자를 클래스에 다시 매핑하기
- 일부 언어에서는 열거형이 이름을 가진 정수처럼 처리되기 때문에 미서드 가질 수없음
- 숫자를 새로운 클래스의 인스턴스로 변환하느 방법이 필요함
- 방법은 열거형에서와 같은 순서로 모든값을 포함하는 배열을 만드는것임
- 즉 숫자를 알맞은 인슽너스에 매핑할 수있음
- 비공개 생성자를 통한 열거
요약
- ‘getter와 setter를 사용하지 말것’ 으로 캡슐화 강제하기란?
- getter와 setter를 사용하지 말것 이란 getter,setter로 간접적으로 비공개 필드 노출해서는 안됨
- ‘getter와 setter 제거하기’ 로 getter제거하기란?
- 이 리팩터링 패턴 사용해서 getter, setter 제거가능함
- ‘공통 접사를 사용하지말것’을 위한 ‘데이터 캡슐화’ 사용이란?
- 공통 접두사, 접미사 있는 메서드 변수는 한 클래스에 함께 있어야함
- ‘데이터 캡술화’리팩터링 패턴 사용가능함
- ‘순서 강제화’로 불변속성제거란?
- 컴파일러가 실행순서를 강제하도록 하는것으로 ‘순서 불변속성’ 제거
- 비공개 생성자 있는 클래스 사용 → 열거형과 스위치문 추가 제거 가능
나눔
- 푸시기반 아키텍처 : 묻지 말고 시켜라 (디미터원칙) → 물으면 데이터 접근위한 체이닝 발생함
'cleanCode > FiveLinesOfCode' 카테고리의 다른 글
[FiveLinesOfCode] 5장 유사한 코드 융합하기 (1) | 2023.10.29 |
---|---|
[FiveLinesOfCode] 4장 타입코드 처리하기 (1) | 2023.10.08 |
[FiveLinesOfCode] 3장 : 긴 코드 조각내기 (1) | 2023.10.03 |
[FiveLinesOfCode] 2장 : 리팩터링 깊게 들여다보기 (0) | 2023.10.03 |
[FiveLinesOfCode] 1장 : 리팩터링, 리팩터링 하기 (0) | 2023.10.03 |