데이터 보호

데이터 보호

  • 데이터와 기능에 대한 접근을 제한하는 캡슐화에 초점을 맞춰 불변속성이 지역에만 영향을 주게 만드는데에 집중

  • 캡슐화를 시행하려면 데이터의 노출을 피해야 한다.

    • getter, setter를 사용하지 말것 규칙은 getter와 setter를 통해 간접적으로 비공개 필드를 노출해서는 안된다는 의미

  • 공통 접사를 사용하지 말것 규칙에 따르면 공통 접두사 또는 접미사가 있는 메서드와 변수가 있을 경우 한 클래스에 함께 있어야 한다.

  • 클래스를 이용한 순서 강제화 리팩터링 패턴을 사용하면 컴파일러가 실행 순서를 강제화할 수 있도록 해서 순서 불변속성을 제거할 수 있다.

Getter 없이 캡슐화하기

getter와 setter 제거하기

  • 부울이 아닌 필드에 setter나 getter를 사용하지 않기

  • 필드를 비공개로 하는 것의 가장 큰 장점은 그렇게 하는 것이 푸시 기반(push-based)의 아키텍처를 장려하기 때문

    • 푸시 기반 아키텍처에서는 가능한 한 데이터에 가깝게 연산을 이관하지만, 풀 기반(pull-based)의 아키텍처에서는 데이터를 가져와 중앙에서 연산을 수행한다.

  • 푸시 기반 아키텍처에서는 데이터를 가져오는 대신 인자로 데이터를 전달한다.

    • 결과적으로 모든 클래스가 자신의 가능을 가지며 코드는 그 효옹에 따라 분산됨

getter는 흔히 private 필드를 다루기 위한 메서드

  • 객체의 필드에 대한 getter가 존재하는 순간 캡슐화를 해제하고 불변속성을 전역적으로 만들게됨

  • 객체를 반환한 후 이를 반환받은 곳에서 이 객체를 더 많은 곳에 전달할 수 있으며 이는 우리가 제어할 수 없게됨

  • 예상하지 못한 방식으로 객체를 수정할 수 있게됨

setter

  • setter를 통한 새로운 데이터 구조를 반환하도록 getter를 수정하는것은 문제가 된다.

  • 그런 다음 수신자 측에서 이 새로운 데이터 구조를 받을 수 있도록 수정해야 한다.

  • 이것은 정확히 우리가 피하고 싶어하는 밀 결합(tight coupling)의 형태이다.

Push vs Pull Architectures

pull-based

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) {
  const url = website.getUrl();
  const user = post.getAuthor();
  const name = user.getUsername();
  const postId = post.getId();
  
  return url + name + postId;
}

push-based

class Website {
  constructor (private url: string) {}
  generateLink(name: string, id: string) {
    return this.url + name + id;
  }
}

class User {
  constructor (private username: string) {}
  generateLink(website: Website, id: string) {
    return website.generateLink(this.username, id);
  }
}

class BlogPost {
  constructor (private author: User, private id: string) {}
  generateLink(website: Website) {
    return this.author.genearteLink(website, this.id);
  }
}

function generatePostLink(website: Website, post: BlogPost) {
  return post.generateLink(website);
}

스멜

  • 이 규칙은 흔히 "낯선 사람에게 말하지 말라"로 요약되는 디미터 법칙에서 유래

    • 디미터 법칙: 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 것을 의미

    • 여기서 낯선 사람이란 우리가 직접 접근할 수는 없지만 참조를 얻을 수 있는 객체를 뜻함

    • 객체지향 언어에서는 이런 일이 가장 일반적으로 getter를 통해 발생하기 때문에 이 규칙이 존재함

의도

  • 참조를 얻을 수 있는 객체와 상호작용할 때의 문제는 객체를 가져오는 방식과 밀 결합되어 있다는 것

  • 우리는 객체 소유자의 내부 구조에 대해 어느정도 알고 있어야 한다.

  • 필드 소유자는 이전 데이터 구조를 획득하는 방법을 계속 지원하지 않는 한 데이터 구조를 변경할 수 없다.

    • 그렇지 않으면 코드가 손상됨

  • 푸시 기반 아키텍처에서는 서비스와 같은 메서드를 노출한다.

    • 이런 메서드의 사용자는 사용하는 메서드 내부구조에 대해 신경쓰지 않아도 된다.

규칙 적용하기

  1. getter 또는 setter가 사용되는 모든 곳에서 오류가 발생하도록 비공개로 설정

  2. 클래스로의 코드 이관으로 오류를 수정

  3. getter 또는 setter는 클래스로의 코드 이관의 일부로 인라인화됨

    1. 따라서 사용하지 않으므로 삭제해서 다른 사람들이 사용하지 않게 설정

// Before
class BlogPost {
  //...
  getAuthor() { return this.author; }
}

// After - getter를 비공개
class BlogPost {
  //...
  private getAuthor() { return this.author; }
}

function generatePostLink(website: Website, post: Blogpost){
  const url = website.url;
  const name = post.getAuthorName();
  const postId = post.getId();
  return url + name + postId
}

class BlogPost {
  //...
  getAuthorName() {return this.author.getUsername();}
}

간단한 데이터 캡슐화하기

코드에는 공통 접두사나 접미사가 있는 메서드나 변수가 없어야 한다.

  • 사용자의 이름의 경우 username, 또는 타이머를 시작할 때의 동작의 경우 startTimer와 같이 해당 컨텍스트를 암시하기 위해 메서드나 변수에 접미사나 접두사를 붙일 때가 많다.

    • 이것은 컨텍스트를 잘 전달하기 위함

  • 코드를 더 읽기 쉽게 만들 순 있지만 여러 요소가 동일한 접사를 가질 때는 그 요소들의 긴밀성을 나타내기도 한다.

  • 이런 구조를 전달하는 더 좋은 방법이 있는데 바로 클래스이다.

  • 클래스를 이용해서 이런 메서드와 변수를 그룹화하는 장점은 외부 인터페이스를 완전하게 제어할 수 있다는 것

    • 도우미 메서드를 숨겨 전역 범위를 오염시키지 않을 수 있다.

  • 가장 중요한것은 데이터를 숨김으로써 해당 불변속성이 클래스 내에서 관리되게 하는 것

  • 그러면 지역 불변속성이 되어 유지보수하기 더 쉬워진다.

// Bad code
function accountDeposit(to: string, amount: number) {
  const accountId = database.find(to);
  database.updateOne(accountId, { $inc: {balance: amount} });
}
function accountTransfer(amount: number, from: string, to: string) {
  accountDeposit(from, -amount);
  accountDeposit(to, amount);
}

// Good
class Account {
  private deposit(to: string, amount: number) {
    const accountId = database.find(to);
    database.updateOne(accountId, { $inc: {balance: amount} });
  }
  
  transfer(amount: number, from: string, to: string) {
    this.deposit(from, -amount);
    this.deposit(to, amount);
  }
}

스멜

  • 이 규칙이 파생된 스멜은 "단일 책임 원칙"이다.

  • 클래스는 단 하나의 책임만 있어야 한다.

의도

  • 단일 책임 원칙(특정 주제의 기능 집합)으로 클래스를 설계하려면 원칙과 개요가 필요

  • 이 규칙은 하위 책임을 식별하는데 도움이 된다.

  • 공통 접사가 암시하는 구조는 해당 메서드와 변수가 공통 접사의 책임을 공유한다는 것을 의미

  • 따라서 이런 메서드는 이 공통의 책임을 전담하는 별도의 클래스에 있어야 한다.

  • 이 규칙은 제품이 커짐에 따라 책임이 발생할 때 어떤 클래스가 책임을 가지는지 파악하는데 도움이 된다.

리팩터링 패턴: 데이터 캡슐화

변수와 메서드를 캡슐화해서 접근할 수 있는 지점을 제한하고 구조를 명확하게 만들 수 있다.

  • 메서드를 캡슐화하면 이름을 단순하게 만들고, 응집력을 더 명확하게 하는데 도움이 된다.

    • 이것은 더 좋은 클래스로 이어진다.

    • 흔히 더 많은 수의 작은 클래스가 도입되는데 이 또한 유익하다.

    • 많은 개발자들은 클래스를 만드는데 너무 소극적이다?

  • 더 많은 위치에서 데이터에 접근할수록 유지보수가 어려워진다.

  • 범위를 제한하면 클래스 내의 메서드만 데이터를 수정할 수 있으므로 이런 메서드들만 그 속성에 영향을 줄 수 있게 된다.

    • 불변속성을 검증해야할 경우 클래스 내부의 코드만 확인하면 된다. (유지보수성 ㅅㅌㅊ)

어떤 상황에선 변수 없이 공통 접사만 존재하는 메서드만 있을순 있는데, 그런 상황에선 이 리팩터링을 사용하는 것이 여전히 타당할 수 있지만, 절차를 수행하기 전에 메서드를 클래스로 이전해야 한다.

절차

  1. 클래스 만듬

  2. 변수를 새로운 클래스로 이동하고 private 접근자 설정

    1. 변수의 이름을 단순한것으로 정하고 변수에 대한 getter, setter를 만든다.

  3. 변수가 더 이상 전역범위에 없기 때문에 컴파일러가 오류를 발생시켜 모든 참조를 찾을 수 있게 해준다.

  4. 오류를 수정한다.

    1. 새 클래스의 인스턴스에 적합한 변수 이름을 선택

    2. 접근을 가상의 변수에 대한 getter 또는 setter로 바꿈

    3. 2개 이상의 다른 메서드에서 오류가 발생한 경우 이전의 변수명을 가진 매개변수를 첫 번째 매개변수로 추가하고 동일한 변수를 첫 번째 인자로 호출하는 쪾에 놓는다.

    4. 한 메서드에서만 오류가 발생할때가지 반복

    5. 변수를 캡슐화했다면 변수가 선언된 지점에서 새로운 클래스를 인스턴스 화한다. 그렇지 않으면 오류가 발생한 메서드에 인스턴스화하는 부분을 만든다.

// Before
let counter = 0;
function incrementCounter() { counter++ }
function main() {
  for (let i=0; i<20; i++) {
    incrementCounter();
    console.log(counter);
  }
}

// After
// 1. 새로운 클래스 생성
class Counter {}
// 2. 변수를 새로운 클래스로 이동 및 private 설정 & getter, setter 추가
class Counter {
  private counter = 0;
  getCounter() {return this.counter};
  setCounter(c: number) {
    this.counter = c;
  }
}
// 3
function incrementCounter() {
  counter.setCounter(counter.getCounter() + 1);
}
function main() {
 for (let i=0; i<20; i++) {
    incrementCounter();
    console.log(counter.getCounter());
  }
}

// 3 - 2개 이상의 메서드가 오류가 있는 경우 case
function incrementCounter(counter: Counter) {
  counter.setCounter(counter.getCounter() + 1);
}
function main() {
 for (let i=0; i<20; i++) {
    let counter = new Counter(); // <- 잘못된 인스턴스 위치
    incrementCounter(counter);
    console.log(counter.getCounter());
  }
}
// 이런 실수하지 않도록 변수를 캡슐화했는지 여부 확인,
// 이 경우 캡슐화했으므로 변수가 있던 지점에서 새로운 클래스를 인스턴스화
class Counter {...}
const counter = new Counter();
// 그런다음 같은 접미사를 가진 incrementCounter를 쉽게 클래스로 이전 가능
  • 필드에 대한 공개적인(public) 접근을 매개변수로 대체하면 결과적으로 필드 없이 메서드를 캡슐화 가능

  • 매개변수로 바꾸면 적합하다고 판단될 경우, 인스턴스화를 필요한 위치로 더 쉽게 이동할 수 있다는 추가적인 이점yes

  • 매개변수 떄문에 클래스를 사용하기 전에 강제로 인스턴스화해야 하므로 클래스를 전역 범위에서 다룰 때 발생할 수 있는 null 참조 오류를 방지할 수 있다.

순서에 존재하는 불변속성 제거하기

  • map이 map.transform 호출로 초기화된다고 한다면, 객체지향 프로그래밍에서 초기화를 위한 다른 메커니즘을 이용하자.

    • 바로 생성자!!

  • 이렇게 하면 다른 메서드보다 먼저 map.transform을 호출해야 하는 불변속성을 제거하는 효과가 있음

  • 무언가가 다른 것보다 먼저 호출되어야 할 때, 그것을 순서 불변속성(sequence invaraint)라고 한다.

    • 생성자를 먼저 호출하지 않는것이 불가능하기 때문에 이 불변속성은 제거 된다.

    • 이것을 순서 강제화 라고 부른다.

class Map {
  constructor() {...}
}
//...
window.onLoad = () => {
  //map.transform(); 호출 제거
  gameLoop(map);
}

리팩터링 패턴: 순서 강제화

  • 가장 멋진 유형의 리팩터링은 컴파일러에게 프로그램이 어떻게 실행되기를 바라는지 가르쳐줄 수 있을 때

    • 그래야 원하는 대로 실행되는지 확인하는데 도움이 된다.

  • 객체지향 언어에서는 생성자가 항상 객체의 메서드보다 먼저 호출되어야 한다.

    • 이 속성을 활용해서 작업이 특정 순서로 발생하게 할 수 있다.

    • 어떤 메서드를 먼저 호출해야하는지 기억할 필요가 없어진다.

    • 순서를 지키지 않는 것이 불가능하기 떄문

  • 생성자를 사용해서 일부 코드가 실행되게 하면 클래스의 인스턴스가 코드가 실행되었다는 증거가 됨

    • 생성자를 성공적으로 실행하지 않고 인스턴스를 얻을수는 없기 때문

function print(str: string) {
  //...str은 대문자로 된 문자열이어야 한다.
  console.log(str);
}

class CapitlizedString {
  private value: string
  constructor(str: string) {
    this.value = capitalize(str);
  }
  
  print() {
    // 불변속성 제거됨
    console.log(this.value);
  }
}
  • 순서 강제화 변환에는 내부와 외부, 두 가지 변형된 버전이 존재

  • 이전 예제는 내부 변형 버전

// 외부 버전
class CapitalizedString {
  private readonly value: string
  constructor(str: string) {
    this.value = capitalize(str);
  }
}

function print(str: CapitalizedString) {
  console.log(str.value);
}

절차

  1. 마지막으로 실행되어야 하는 메서드에 데이터 캡슐화를 적용

  2. 생성자가 첫 번째 메서드를 호출하도록 설정

  3. 두 메서드의 인자가 연결되어 있으면 이러한 인자를 필드로 만들고 메서드에서 제거

// 돈을 받는 사람의 잔고에서 금액을 더하기 전에 
// 항상 보낸 사람의 잔고에서 먼저 금액을 빼야 한다고 생각해보자. 
// 따라서 순서는 금액의 음수 값으로 deposit을 호출한 후 금액의 양수 값으로 deposit을 호출해야 한다.
function deposit(to: string, amount: number) {
  const accountId = database.find(to);
  database.updateOne(accountId, { $inc: { balance: amount }});
}

// 1. 마지막에 실행되어야할 메서드에 데이터 캡슐화 적용
class Transfer {
  deposit(to: string, amount: number) {
    const 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) {
    const accountId = database.find(to);
    database.updateOne(accountId, { $inc: { balance: amount }});
  }
}
  • 송금자 측 계좌에서 음수의 amount로 deposit이 먼저 호출된다는 것을 보증할수 있게 됨

  • 더 개선할 수 있는 방법으로 amount 인자를 필드로 만들고 deposit 메서드에서 amount를 제거하여 두 amount를 연결할 수 있다.

    • 한 번은 음수의 amount가 필요하기 때문에 도우미 메서드를 도입

class Transfer {
  constructor(from: string, private amount: number) {
    this.depositHelper(from, -this.amount);
  }
  private depositHelper(to: string, amount: number) {
    const accountId = database.find(to);
    database.updateOne(accountId, { $inc: { balance: amount }})
  }
  deposit(to: string) {
    this.depositHelper(to, this.amount);
  }
}
  • 출금 없이 입금될 수 없다는 것을 보증할 수 있게 되었지만 수취인(to)을 인자로 deposit을 호출하는 것을 잊어버리면 돈이 그냥 사라질 수 있게 됨

  • 따라서 입금도 반드시 발생하도록 이 클래스를 다른 클래스로 감쌀수도 있다.

열거형을 제거하는 또 다른 방법

열거형은 메서드를 가질 수 없다.

비공개 생성자를 통한 열거

  • 사용하는 언어가 열거형에 대해 메서드를 지원하지 않는 경우 private 생성자를 사용해서 이를 우회할 수 있는 기법 존재

  • 모든 객체는 생성자를 호출해서 생성해야 한다.

  • 생성자를 private로 만들면 클래스 내부에서만 객체를 생성할 수 있게 된다.

    • 특히 존재하는 인스턴스 수를 제어할 수 있게 됨(싱글톤)

    • 이를 인스턴스를 공개(public) 상수에 넣으면 열거형으로 사용할 수 있다.

enum TShirtSize {
  SMALL,
  MEDIUM,
  LARGE
}
function sizeToString(s: TShirtSize) {
  if (s === TShirtSize.SMALL)
    return "S";
  else if(s === TShirtSize.MEDIUM)
    return "M";
  else if(s === TShirtSize.LARGE)
    return "L";
}

// 비공개 생성자
// 클래스에 코드를 넣을수 있게 됨
class TShirtSize {
  static readonly SMALL = new TSirtSize();
  static readonly MEDIUM = new TSirtSize();
  static readonly LARGE = new TSirtSize();
  private constructor() {}
}
//비공개 생성자 switch는 사용할 수 없게됨, 괜찮음 스위치는 사용하지 않는 규칙이 있으니
function sizeToString(s: TShirtSize) {
  if (s === TShirtSize.SMALL)
    return "S";
  else if(s === TShirtSize.MEDIUM)
    return "M";
  else if(s === TShirtSize.LARGE)
    return "L";
}

interface SizeValue {}
class SmallValue implements SizeValue {}
class MediumValue implements SizeValue {}
class LargeValue implements SizeValue {}
// 네임스페이스나 패키지를 사용해서 이런 이름을 단순화도 가능

class TShirtSize {
  static readonly SMALL = new TSirtSize(new SmallValue());
  static readonly MEDIUM = new TSirtSize(new MediumValue());
  static readonly LARGE = new TSirtSize(new LargeValue());
  private constructor(private value: SizeValue) {}
}

Last updated