Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
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
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[비트캠프] 46일차(10주차1일) - Java(캡슐화, getter/setter, 접근 범위), app-11~13, backend-app-01~02 본문

네이버클라우드 AIaaS 개발자 양성과정 1기/Java

[비트캠프] 46일차(10주차1일) - Java(캡슐화, getter/setter, 접근 범위), app-11~13, backend-app-01~02

끈기JK 2023. 1. 9. 11:24

 

myapp

 

11. Domain 클래스에 getter/setter 적용

 

Board 의 필드를 캡슐화하기 위해 private으로 만들고 getter/setter 설정하여 값을 설정하고 꺼낸다.

 

### 11. 도메인 클래스에 게터/세터 적용하기 
- 캡슐화 문법을 이용하여 필드의 접근을 제어하는 방법 
- 메서드를 통해 필드의 값을 설정하고 꺼내는 방법

 

package bitcamp.myapp;

// 회원 데이터를 담을 메모리를 설계한다.
public class Member {
  private int no;
  private String name;
  private String tel;
  private String postNo;
  private String basicAddress;
  private String detailAddress;
  private boolean working;
  private  char gender;
  private byte level;
  private String createdDate;

  public int getNo() {
    return no;
  }
  public void setNo(int no) {
    this.no = no;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getTel() {
    return tel;
  }
  public void setTel(String tel) {
    this.tel = tel;
  }
  public String getPostNo() {
    return postNo;
  }
  public void setPostNo(String postNo) {
    this.postNo = postNo;
  }
  public String getBasicAddress() {
    return basicAddress;
  }
  public void setBasicAddress(String basicAddress) {
    this.basicAddress = basicAddress;
  }
  public String getDetailAddress() {
    return detailAddress;
  }
  public void setDetailAddress(String detailAddress) {
    this.detailAddress = detailAddress;
  }
  public boolean isWorking() {
    return working;
  }
  public void setWorking(boolean working) {
    this.working = working;
  }
  public char getGender() {
    return gender;
  }
  public void setGender(char gender) {
    this.gender = gender;
  }
  public byte getLevel() {
    return level;
  }
  public void setLevel(byte level) {
    this.level = level;
  }
  public String getCreatedDate() {
    return createdDate;
  }
  public void setCreatedDate(String createdDate) {
    this.createdDate = createdDate;
  }
}

 

package bitcamp.myapp;

public class Board {
  private int no;
  private String title;
  private String content;
  private String password;
  private String createdDate;
  private int viewCount;

  public int getNo() {
    return no;
  }
  public void setNo(int no) {
    this.no = no;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getCreatedDate() {
    return createdDate;
  }
  public void setCreatedDate(String createdDate) {
    this.createdDate = createdDate;
  }
  public int getViewCount() {
    return viewCount;
  }
  public void setViewCount(int viewCount) {
    this.viewCount = viewCount;
  }
}

 

 

 

com.eomcs.oop.ex08.a

 

 

Encapsulation + getter/setter

 

 

멤버에 대한 접근 제어 : (default)

 

Score 클래스의 compute() 는 Score 의 필드에 접근 ok 이다. 같은 멤버이기 때문이다.

Exam0110의 main() 도 접근 ok 이다. 같은 멤버는 아니지만, Score에서 허락했기 때문이다.

여기서 허락이란? (default) 선언으로 같은 멤버 + 같은 패키지에서 접근 가능하다.

 

  • 멤버 접근 제어
    • private: 같은 멤버만
    • (default): 같은 멤버 + 같은 패키지
    • protected: 같은 멤버 + 같은 패키지 + 서브 클래스
    • public

 

 

# 캡슐화 문법 사용 전 - 개발자가 클래스를 작성한 사람의 의도대로 정상적으로 사용할 때
package com.eomcs.oop.ex08.a;

class Score {
  String name;
  int kor;
  int eng;
  int math;
  int sum;
  float aver;

  void compute() {
    this.sum = this.kor + this.eng + this.math;
    this.aver = this.sum / 3f;
  }
}

public class Exam0110 {
  public static void main(String[] args) {

    // 1) 개발자가 새로 정의한 데이터 타입에 따라 변수를 준비한다.
    Score s1 = new Score();

    // 2) 레퍼런스에 저장된 인스턴스 주소로 찾아가서
    //    각 변수의 값을 설정한다.
    s1.name = "홍길동";
    s1.kor = 100;
    s1.eng = 90;
    s1.math = 80;

    // 3) compute() 연산자를 사용하여 새 데이터 타입의 값을 다룬다.
    //    값.연산자();   ===> 예) i++;
    // - compute()는 Score 설계도에 따라 만든 변수의 값을 다룬다.
    //   즉 변수의 값을 다루는 연산자 역할을 수행한다.
    // - 그래서 인스턴스 메서드를 "연산자(operator)"라 부른다.
    s1.compute();

    System.out.printf("%s, %d, %d, %d, %d, %.1f\n",
        s1.name, s1.kor, s1.eng, s1.math, s1.sum, s1.aver);

  }
}

 

 

# 캡슐화 문법 사용 전 - 클래스를 작성한 사람의 의도와 다르게 사용할 때
package com.eomcs.oop.ex08.a;

public class Exam0210 {
  public static void main(String[] args) {

    Score s1 = new Score();

    s1.name = "홍길동";
    s1.kor = 100;
    s1.eng = 90;
    s1.math = 80;

    s1.compute();

    // 계산을 한 후에 임의적으로 합계나 평균을 변경한다면?
    s1.aver = 100f;

    // 원래 프로그램에서 의도한 대로 계산 결과가 나오지 않는다!
    System.out.printf("%s, %d, %d, %d, %d, %.1f\n",
        s1.name, s1.kor, s1.eng, s1.math, s1.sum, s1.aver);
  }
}

해결책!
- sum이나 aver 필드처럼 기존 필드의 값을 연산해서 나온 결과를 저장하는 경우, 직접 해당 변수의 값을 변경하지 못하도록 막아야 한다.
- 오직 메서드를 통해서만 변경하도록 해야 한다.

 

 

# 캡슐화 문법 사용 후 - 개발자가 특정 필드를 직접 접근하지 못하게 막기
package com.eomcs.oop.ex08.a;

class Score2 {
  String name;
  int kor;
  int eng;
  int math;

  // sum 이나 aver 필드는 kor, eng, math 값을 연산한 결과를 보관하기 때문에
  // 직접 접근하여 값을 변경하는 것을 허용해서는 안된다.
  // 허용하는 순간 개발자의 잘못된 명령으로
  // 국,영,수 점수와 합계, 평균이 서로 맞지 않는 문제가 발생할 수 있다.
  // 그래서 자바는 필드나 메서드의 외부 접근 범위를 조정하는 문법을 제공한다.
  // 그 문법을 '캡슐화(encapsulation)'라 부른다.
  //
  private int sum;
  private float aver;

  // sum과 aver의 값을 직접 변경하지 못하게 막았으면,
  // 외부에서 이 값들을 조회할 수 있는 방법/수단(method)은 제공해야 한다.
  // => 보통 이렇게 필드의 값을 조회하는 용도로 사용하기 위해 메서드를 만들 경우
  //    메서드의 용도를 이해하기 쉽도록 getXxx() 형태로 이름을 짓는다.
  //       get필드명() {...}
  // => 메서드의 이름이 get 으로 시작한다고 해서 "게터(getter)"라고 부른다.
  // => 그리고 이런 getter는 공개 모드로 설정한다.
  //
  public int getSum() {
    return this.sum;
  }

  public float getAver() {
    return this.aver;
  }

  void compute() {
    this.sum = this.kor + this.eng + this.math;
    this.aver = this.sum / 3f;
  }
}

public class Exam0211 {
  public static void main(String[] args) {

    Score2 s1 = new Score2();

    s1.name = "홍길동";
    s1.kor = 100;
    s1.eng = 90;
    s1.math = 80;

    s1.compute();

    // 계산을 한 후에 임의적으로 합계나 평균을 변경한다면?
    // => sum과 aver 필드는 private 접근만 허용한다.
    // => 즉 해당 필드와 같은 클래스에 소속된 멤버만이 접근할 수 있고
    //    외부 클래스에서는 접근할 수 없다.
    // => 그래서 다음과 같이 임의로 접근하여 값을 변경할 수 없다.
    //
    //    s1.sum = s1.kor + s1.eng + s1.math; // 컴파일 오류!
    //    s1.aver = s1.sum / 4f; // 컴파일 오류!

    System.out.printf("%s, %d, %d, %d, %d, %.1f\n",
        s1.name, s1.kor, s1.eng, s1.math, s1.getSum(), s1.getAver());
  }
}

 

 

# 캡슐화 문법 사용 전 - 개발자가 특정 필드를 직접 접근하지 못하게 막기
package com.eomcs.oop.ex08.a;

public class Exam0310 {
  public static void main(String[] args) {

    Score2 s1 = new Score2();

    s1.name = "홍길동";
    s1.kor = 100;
    s1.eng = 90;
    s1.math = 80;

    s1.compute();

    // 캡슐화 문법으로 sum과 aver의 값을 임의적으로 조작하는 것은 막았다.
    // 그런데 또 다른 문제가 있다.
    // 만약 개발자가 국, 영, 수 점수를 변경한 후
    // compute()를 호출하지 않는다면?
    //
    s1.eng = 100;
    s1.math = 100;

    // 원래 Score2의 개발자 의도대로 사용한다면
    // 다음과 같이 국, 영, 수 점수를 변경한 후 compute()를 호출했어야 한다.
    //    s1.compute();
    // 그런데 이렇게 하지 않는 경우가 문제가 되는 것이다.

    System.out.printf("%s, %d, %d, %d, %d, %.1f\n",
        s1.name, s1.kor, s1.eng, s1.math, s1.getSum(), s1.getAver());
  }
}

 

 

# 캡슐화 문법 사용 후 - 필드의 값을 변경할 때 마다 특정 작업을 수행하게 만들기
package com.eomcs.oop.ex08.a;

class Score3 {
  String name;

  // 국, 영, 수 점수를 바꿀 때 마다 자동으로 합계, 평균을 계산해야 한다.
  // 방법?
  // - 직접 필드의 값을 바꾸게 하지 말고 메서드를 통해 바꾸도록 유도한다.
  // - 이렇게 필드의 값을 바꿀 때 마다 뭔가를 수행해야 하는 경우,
  //   해당 필드의 직접 접근을 막아라!
  private int kor;
  private int eng;
  private int math;

  // 대신 메서드를 통해 값을 설정하게 하라!
  // 보통 필드의 값을 설정하는 메서드는 'set필드명()'으로 이름을 짓는다.
  // - 이런 메서드를 "세터(setter)"라 부른다.
  // - 외부에서 호출할 수 있도록 공개 모드로 설정한다.
  // - 필드를 비공개로 막으면 값을 조회할 수 없기 때문에
  //   getter도 추가해야 한다.
  //
  public void setKor(int kor) {
    this.kor = kor;
    this.compute();
  }

  public int getKor() {
    return this.kor;
  }

  public void setEng(int eng) {
    this.eng = eng;
    this.compute();
  }

  public int getEng() {
    return this.eng;
  }

  public void setMath(int math) {
    this.math = math;
    this.compute();
  }

  public int getMath() {
    return this.math;
  }


  private int sum;
  private float aver;

  public int getSum() {
    return this.sum;
  }

  public float getAver() {
    return this.aver;
  }

  // 공개할 필요가 없는 메서드는 private으로 막아라.
  // 보통 private 으로 막는 메서드는 해당 클래스 내부에서만 사용되는 메서드이다.
  private void compute() {
    this.sum = this.kor + this.eng + this.math;
    this.aver = this.sum / 3f;
  }
}

public class Exam0311 {
  public static void main(String[] args) {

    Score3 s1 = new Score3();

    s1.name = "홍길동";
    s1.setKor(100);
    s1.setEng(90);
    s1.setMath(80);

    // 세터를 통해서 국, 영, 수 값을 설정할 때마다
    // 합계와 평균을 자동으로 계산하기 때문에 직접 compute()를 호출할 필요가 없다.
    //    s1.compute();

    // 다음과 같이 언제든지 국, 영, 수 점수를 변경하더라도
    // 합계와 평균이 자동계산될 것이다.
    s1.setEng(100);
    s1.setMath(100);

    // 이제 kor, eng, math 도 비공개 모드이기 때문에
    // 값을 조회하려면 게터를 사용해야 한다.
    //
    System.out.printf("%s, %d, %d, %d, %d, %.1f\n",
        s1.name,
        s1.getKor(), s1.getEng(), s1.getMath(),
        s1.getSum(), s1.getAver());
  }
}

 

 

# 캡슐화 문법 사용 후 - 캡슐화와 게터/세터
package com.eomcs.oop.ex08.a;

class Score4 {

  private String name;
  // name은 직접 접근해도 되는데, 프로그래밍의 일관성을 위해
  // 다른 필드처럼 직접 접근을 막고 getter/setter를 통해 값을 다루도록 한다.
  // 이렇게 하면 나중에 name에 대해
  // 값의 유효성을 검사하는 코드를 즉시 삽입할 수 있어 유지보수에도 도움이 된다.
  //
  // 그래서 실무에서는 그냥 모든 필드를 private이나 protected로 접근을 제한한 다음에
  // setter/getter를 두는 방식으로 프로그래밍을 한다.
  // setter/getter가 필요없는 필드라도 그냥 관성적으로 그렇게 한다.
  // 고민하지 말고 여러분도 그냥 이렇게 하라!
  // setter에서 유효성을 검사하지 않더라도 그냥 만들라!
  //
  // - 즉 필드는 캡슐화 문법을 통해 외부의 접근 제한하고,
  //   세터/게터 메서드를 통해 값을 설정/조회하게 만든다.
  // - name, kor, eng, math 필드 같은 경우
  //   값을 설정하고 조회도 해야 하기 때문에 게터/세터가 모두 있다.
  // - sum, aver 필드 같은 경우
  //   값을 조회만 해야 하기 때문에 게터만 있다.
  // - 이렇게 필드에 대해서 항상 게터/세터를 모두 만드는 것이 아니다.
  //
  // 용어 주의!
  // - name, kor, eng, math, sum, aver 는 "필드(field)" 라 부른다.
  // - getXxx()/setXxx()는 "프로퍼티(property)"라 부른다.
  // - 필드를 프로퍼티라 부르는 것이 아니다!
  //   게터/세터를 프로퍼티라 부르는 것이다.

  // 점수를 변경할 때 계산을 다시 해야 하고, 유효하지 않은 값을 넣지 못하도록 막아야 한다.
  // 따라서 직접 접근하는 것을 막는다.
  private int kor;
  private int eng;
  private int math;

  // 계산 결과를 조작하지 못하도록 접근을 제한하자!
  private int sum;
  private float aver;

  // 대신 결과 값을 꺼낼 수 있는 메서드(getter)를 제공해야 한다.

  public void setName(String name) {
    // 이렇게 유효성을 검사하지 않더라도 setter를 그냥 만들라!
    this.name = name;
  }

  public String getName() {
    return this.name;
  }

  public int getKor() {
    return this.kor;
  }
  public void setKor(int kor) {
    if (kor >= 0 && kor <= 100) { // 유효한 점수인 경우에만 저장한다.
      this.kor = kor;
      this.compute(); // 유효한 값이라면 다시 합계와 평균을 계산한다.
    }
  }

  public int getEng() {
    return this.eng;
  }
  public void setEng(int eng) {
    if (eng >= 0 && eng <= 100) { // 유효한 점수인 경우에만 저장한다.
      this.eng = eng;
      this.compute(); // 유효한 값이라면 다시 합계와 평균을 계산한다.
    }
  }

  public int getMath() {
    return this.math;
  }
  public void setMath(int math) {
    if (math >= 0 && math <= 100) {// 유효한 점수인 경우에만 저장한다.
      this.math = math;
      this.compute(); // 유효한 값이라면 다시 합계와 평균을 계산한다.
    }
  }

  public int getSum() {
    return this.sum;
  }

  public float getAver() {
    return this.aver;
  }

  // 점수를 변경할 때 마다 호출되기 때문에 임의로 호출할 필요가 없다.
  // 따라서 비공개로 만든다.
  // 초보 개발자의 많은 착각!
  // - 필드는 무조건 private,
  //   메서드는 무조건 public 으로 해야 한다고 생각한다.
  // - 착각이다.
  //   필드든 메서드든 공개할 것은 공개하고 공개하지 말아야 하는 것은 공개하지 말라.
  // - 기본이 비공개이고, 공개할 것만 공개하라!
  //   이렇게 하는 것이 클래스가 잘못 사용되는 상황을 방지할 수 있다.
  //
  private void compute() {
    this.sum = this.kor + this.eng + this.math;
    this.aver = this.sum / 3f;
  }
}

public class Exam0410 {
  public static void main(String[] args) {

    Score4 s1 = new Score4();

    s1.setName("홍길동");
    s1.setKor(100);
    s1.setEng(90);
    s1.setMath(80);

    System.out.printf("%s, %d, %d, %d, %d, %.1f\n",
        s1.getName(),
        s1.getKor(), s1.getEng(), s1.getMath(),
        s1.getSum(), s1.getAver());
  }
}

 정리!
 # 객체지향 프로그래밍(Object-Oriented Programming; OOP)의 특징
 1) 추상화(abstraction)
   - 프로그램에서 다루는 데이터나 코드를 클래스로 정의하는 행위.
   - 클래스 멤버(스태틱 멤버) : 스태틱 필드, 스태틱 블록, 스태틱 메서드
   - 인스턴스 멤버 : 인스턴스 필드, 인스턴스 블록, 인스턴스 메서드, 생성자

 2) 상속(inheritance)
   - 기능 확장을 위한 문법

 3) 캡슐화(encapsulation)
   - 외부의 접근을 제어하는 문법

 4) 다형성(polymorphism)
   - 하나의 코드가 여러 용도로 쓰이게 하는 것.
   - 오버로딩(overloading)
     - 메서드 시그너처가 다르더라도 같은 기능을 하는 메서드에 대해 같은 이름을 갖게하여
       일관성 있는 프로그래밍을 하게 도와주는 문법.
   - 오버라이딩(overriding)
     - 상속 받은 메서드를 자신의 역할에 맞게 재정의 하는 것.
   - 다형적 변수(polymorphic variable)
     - 하나의 변수가 여러 타입을 가리킬 수 있고, 다양한 타입으로 다뤄질 수 있게 도와주는 문법.

 # 캡슐화
 - 클래스 멤버나 인스턴스 멤버의 접근을 제한하는 문법이다.
 - 이유? 잘못된 사용으로 결과가 왜곡되는 것을 막기 위함이다.
 - 정의된 대로 역할을 수행하게 도와준다.
 - 문법: 클래스 멤버나 인스턴스 멤버 선언할 때 접근 제한자(modifier)를 붙인다.
 - 접근 제한자
   - private : 클래스에 소속된 같은 멤버만 접근 가능
   - (default) : 같은 패키지에 소속된 멤버만 접근 가능
   - protected : 같은 패키지에 소속되거나 자손 클래스의 멤버만 접근 가능
   - public : 모두 접근 가능

 # getter/setter
 - 캡슐화와 더불어 사용되는 기술이다.
 - 필드에 대해 외부의 직접적인 접근을 막는 대신 메서드를 통해 값을 변경, 조회하도록 유도한다.
 - 메서드에서 값의 유효 범위를 검사하여 변경을 허락할 수 있다.
 - getter/setter 를 다른 말로 "프로퍼티(property)"라 부른다.
   - getter만 있는 경우: read only 프로퍼티
   - setter만 있는 경우: write only 프로퍼티
   - getter/setter 모두 있는 경우: read/write 프로퍼티

 

 

캡슐화(encapsulation) - 필요한 이유
package com.eomcs.oop.ex08.b;

//class 문법을 이용하여 병원 고객을 추상화하였다.
class Customer {
  String name;
  int age;
  int weight;
  int height;
}

public class Exam0110 {

  public static void main(String[] args) {
    // 환자 데이터를 등록해보자!
    Customer c1 = new Customer();
    c1.name = "홍길동";
    c1.age = 300;
    c1.weight = 100;
    c1.height = -50;

  }
}

 위의 코드의 문제점을 분석!
 => 각각의 값이 인스턴스 변수에 들어갈 수 있는 값이기 때문에 컴파일 오류는 발생하지 않는다.
 => 그러나, "환자" 데이터로서 유효한 값은 아니다!
 => 위의 데이터는 거의 괴물 데이터라 볼 수 있다.
    즉 환자를 추상화시킨 목적을 상실한 것이다.
    즉 추상화가 무너진 것이다!
 => Customer는 환자 데이터를 다루기 위해서 만든 클래스이지,
    괴물이나 비과학적 데이터를 다루기 위해서 만든 클래스가 아니다.
    즉 유효하지 않은 데이터를 넣게 되면 클래스를 정의한 이유를 상실하게 된다.
 이를 방지하기 위해 만든 문법이 "캡슐화(encapsulation)"이다.

 캡슐화?
 => 인스턴스의 변수에 추상화 목적에 맞는 유효한 값만 넣을 수 있도록 외부 접근을 제한하는 문법이다.
 => 제한 범위
    private   : 클래스 내부에서만 접근 가능
    (default) : 클래스 내부 + 같은 패키지
    protected : 클래스 내부 + 같은 패키지 + 자식클래스
    public    : 모두 접근 가능!

 

 

캡슐화(encapsulation) - 접근 제한
package com.eomcs.oop.ex08.b;

class Customer2 {
  // 외부에서 인스턴스 변수에 직접 접근하지 못하도록 막는다!
  private String name;
  private int age;
  private int weight;
  private int height;
}

public class Exam0120 {

  public static void main(String[] args) {
    // 환자 데이터를 등록해보자!
    Customer2 c1 = new Customer2();

    // Customer2에서는 인스턴스 변수의 접근을 private으로 막았기 때문에
    // 다른 클래스는 해당 인스턴스 변수에 접근할 수 없다.
    //
    //    c1.name = "홍길동";
    //    c1.age = 300;
    //    c1.weight = 100;
    //    c1.height = -50;

    // 해결책?
    // => 이 클래스에서 제공하는 메서드를 사용하라!
    // => Exam0130.java를 확인하라!
  }
}

 

 

캡슐화(encapsulation) - 셋터와 겟터
package com.eomcs.oop.ex08.b;

class Customer3 {
  // 외부에서 인스턴스 변수에 직접 접근하지 못하도록 막는다!
  private String name;
  private int age;
  private int weight;
  private int height;

  // 외부에서 인스턴스 변수에 접근을 못하기 때문에 값을 넣거나 조회할 수 없다.
  // 그래서 이를 가능하게 하는 수단/방법(method)을 제공해야 한다.
  // => 보통 메서드 명은 set으로 시작한다.
  // => 그래서 이 메서드를 "셋터(setter)"라고 부른다.

  // 인스턴스 변수 name의 값을 넣는 메서드
  public void setName(String name) {
    // 이 메서드에서 이름 값이 유효한지 검사한다.
    if (name == null) {
      this.name = "이름없음";
      return;
    }

    if (name.length() < 2) {
      this.name = "이름없음";
      return;
    }

    // 이름은 최대 5자만 넣는다.
    if (name.length() > 5) {
      this.name = name.substring(0, 5);
    } else {
      this.name = name;
    }
  }

  // 인스턴스 변수 age의 값을 넣는 메서드
  // => 나이 값이기 때문에 유효한 값은 1 ~ 150이다.
  public void setAge(int age) {
    if (age < 1 || age > 150) {
      this.age = 0;
      return;
    }
    this.age = age;
  }

  // 인스턴스 변수 weight의 값을 넣는 메서드
  // => 몸무게는 1 ~ 200 이다.
  public void setWeight(int weight) {
    if (weight < 1 || weight > 200) {
      this.weight = 0;
      return;
    }
    this.weight = weight;
  }

  // 인스턴스 변수 height의 값을 넣는 메서드
  // => 키의 유효 범위는 1 ~ 300 이다.
  public void setHeight(int height) {
    if (height < 1 || height > 300) {
      this.height = 0;
      return;
    }
    this.height = height;
  }

  // 외부에서 인스턴스 변수의 값을 조회할 수 있는 메서드를 제공한다.
  // => 보통 메서드의 이름은 get으로 시작한다.
  // => 그래서 "겟터(getter)"라 부른다.

  public String getName() {
    return this.name;
  }

  public int getAge() {
    return this.age;
  }

  public int getHeight() {
    return this.height;
  }

  public int getWeight() {
    return this.weight;
  }

}

public class Exam0130 {

  public static void main(String[] args) {
    // 환자 데이터를 등록해보자!
    Customer3 c1 = new Customer3();

    // 인스턴스 변수에 직접 접근할 수 없기 때문에 메서드를 통해 값을 넣어야 한다.
    // => Customer3에는 인스턴스 변수의 값을 설정할 수 있는 셋터가 구비되어 있다.
    c1.setName("홍길동");
    c1.setAge(300);
    c1.setWeight(-100);
    c1.setHeight(-50);
    // 셋터 메서드에서 유효한 값이 아니면 필터링하여 처리할 것이다.

    // 값을 꺼내보자!
    // => 인스턴스 변수에 직접 접근할 수 없기 때문에 메서드를 통해 값을 꺼내야 한다.
    // => Customer3에는 인스턴스 변수의 값을 리턴해주는 겟터가 구비되어 있다.
    System.out.printf("%s, %d, %d, %d\n",
        c1.getName(), c1.getAge(), c1.getWeight(), c1.getHeight());
  }
}

 

 

캡슐화(encapsulation) - 셋터와 겟터

 실무에서는 셋터에서 유효 값을 검증하는 코드를 잘 넣지 않는다.
 따로 인스턴스 변수의 값을 검증하는 메서드를 추가하여 처리한다.
 그래서 실무에서 셋터 메서드는 인스턴스 변수에 그냥 값을 넣는 경우가 많다.
 즉 인스턴스 변수에 직접 값을 넣는 것과 동일하게 동작한다.
 이런 상황 때문에 셋터, 겟터의 무용론을 주장하는 개발자들이 있다.
 그들은 그냥 인스턴스 변수의 접근 범위를 public으로 공개하여 사용할 것을 주장한다.
 그러나 대부분의 개발자들은 셋터의 무용함을 떠나, 메서드를 통해 변수의 값을 설정하는 방법을 선호한다.
 혹 나중에 메서드에 기타 코드를 추가할 경우를 대비하기 위함이다.
 변수를 직접 사용하면 변수를 제어하는 코드를 삽입하기 어렵기 때문이다.

package com.eomcs.oop.ex08.b;

class Customer4 {

  // 외부에서 인스턴스 변수에 직접 접근하지 못하도록 막는다!
  private String name;
  private int age;
  private int weight;
  private int height;

  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public int getAge() {
    return age;
  }
  public void setAge(int age) {
    this.age = age;
  }
  public int getWeight() {
    return weight;
  }
  public void setWeight(int weight) {
    this.weight = weight;
  }
  public int getHeight() {
    return height;
  }
  public void setHeight(int height) {
    this.height = height;
  }
}

public class Exam0140 {

  public static void main(String[] args) {
    // 환자 데이터를 등록해보자!
    Customer4 c1 = new Customer4();

    // 실무에서 만드는 셋터는 보통 파라미터 값을 검증하지 않기 때문에
    // 그냥 입력된 값 그대로 인스턴스 변수에 저장한다.
    // 그래서 값을 꺼내 출력해보면 입력된 값 그대로 출력될 것이다.
    c1.setName("홍길동");
    c1.setAge(300);
    c1.setWeight(100);
    c1.setHeight(-50);

    System.out.printf("%s, %d, %d, %d\n",
        c1.getName(), c1.getAge(), c1.getWeight(), c1.getHeight());
  }
}

 

 

 

접근 범위 테스트

 

ex08 패키지 내에 있는 X 클래스와 Exam0211 클래스가 있다. Exam0211 에서 X 의 privateVar 는 접근 불가하지만, defaultVar, protectedVar, publicVar, test() 모두 접근 가능하다.

 

ex08 패키지 내에 있는 sub 내의 Y 클래스가 있고, ex08 패키지 내에 Exam0212 클래스가 있다. Exam0212 에서 Y 의 privateVar, defaultVar, protectedVar 에 접근 불가하지만 publicVar 에 접근 가능하다.

 

 

캡슐화 접근 범위 테스트 - 같은 패키지의 멤버가 접근할 수 있는 범위
package com.eomcs.oop.ex08.b;

public class Exam0211 {

  public static void main(String[] args) {
    X obj = new X();

    //    obj.privateVar = 100; // 접근 불가! 오직 그 클래스 안에서만 사용가능.
    obj.defaultVar = 100; // OK! 이 클래스는 A 클래스와 같은 패키지에 소속되어 있다.
    obj.protectedVar = 100; // OK! 비록 이 클래스가 자식클래스는 아니지만 같은 패키지에 소속되어 있다.
    obj.publicVar = 100; // OK! 모두 다 접근 가능!
  }
}

 private      : 클래스 안에서만 접근 가능
 (default)    : private + 같은 패키지 소속
 protected    : (default) + 서브 클래스로 만든 변수인 경우 서브 클래스에서 접근 가능
 public       : 모두 접근 가능

 실무
 => 인스턴스 변수는 보통 private 으로 접근을 제한한다.
 => 겟터,셋터는 public으로 접근을 모두에게 공개한다.
 => 일반 메서드도 public으로 접근을 모두에게 공개한다.
 => 그 클래스 내부에서만 사용되는 메서드는 private으로 접근을 제한한다.
 => 자식 클래스의 접근을 허용할 필요가 있을 경우에만 protected로 만든다.
 => 다른 개발자가 사용할 클래스 모음을 만들 때
    그 모음집 내에서만 사용될 변수나 메서드인 경우 (default)로 접근을 제한한다.
    즉 라이브러리를 만드는 개발자인 경우 (default)를 사용하는 경우가 있다.

 

package com.eomcs.oop.ex08.b;

public class X {
  private int privateVar;
  int defaultVar;
  protected int protectedVar;
  public int publicVar;

  private void privateMethod() {}
  void defaultMethod() {}
  protected void protectedMethod() {}
  public void publicMethod() {}

  public void test() {
    // 같은 클래스의 멤버(필드나 메서드)인 경우 제한없이 모든 멤버에 접근할 수 있다.
    this.privateVar = 100;
    this.defaultVar = 100;
    this.protectedVar = 100;
    this.publicVar = 100;

    this.privateMethod();
    this.defaultMethod();
    this.protectedMethod();
    this.publicMethod();
  }
}

 

 

캡슐화 접근 범위 테스트 - 다른 패키지의 멤버가 접근할 수 있는 범위
package com.eomcs.oop.ex08.b;

public class Exam0212 {

  public static void main(String[] args) {
    // B 클래스는 Exam0212와 다른 패키지이다.
    com.eomcs.oop.ex08.b.sub.Y obj = new com.eomcs.oop.ex08.b.sub.Y();

    //    obj.privateVar = 100; // 접근 불가! 오직 그 클래스 안에서만 사용 가능.
    //    obj.defaultVar = 100; // 접근 불가! 같은 패키지까지만 접근 가능.
    //    obj.protectedVar = 100; // 접근 불가! 같은 패키지 또는 자식 클래스 접근 가능
    obj.publicVar = 100; // OK! 모두 다 접근 가능.
  }
}

 

package com.eomcs.oop.ex08.b.sub;

public class Y {
  private int privateVar;
  int defaultVar;
  protected int protectedVar;
  public int publicVar;
}

 

 

 

접근 범위 테스트 : protected 접근

 

ex08 - sub - Y 가 있고 ex08 - Exam0213 이 있다. Y(super class)를 Exam0213(sub class)에서 상속한다.

new Y() 로 privateVar, defaultVar, protectedVar, publicVar 가 생성된다. Exam0213에서 publicVar 외에 접근 불가하다. protectedVar 또한 접근 불가한데 서브클래스 설계도에 따라 생성한 변수인 경우에만 접근 가능하다.

 

 

캡슐화 접근 범위 테스트 - 서브 클래스의 멤버가 접근할 수 있는 범위
package com.eomcs.oop.ex08.b;

public class Exam0213 extends com.eomcs.oop.ex08.b.sub.Y {

  public static void main(String[] args) {

    // 다른 패키지의 클래스를 그 클래스의 서브 클래스가 어디까지 접근할 수 있을까?
    com.eomcs.oop.ex08.b.sub.Y obj = new com.eomcs.oop.ex08.b.sub.Y();


    //    obj.privateVar = 100; // 접근 불가! 오직 그 클래스 안에서만 사용 가능.
    //    obj.defaultVar = 100; // 접근 불가! 같은 패키지까지만 접근 가능.

    //    obj.protectedVar = 100; // 접근 불가! 같은 패키지 또는 자식 클래스 접근 가능
    // 자식 클래스인데 접근 불가?
    // 이유 => 서브 클래스를 통해 만든 인스턴스 변수가 아니다.

    obj.publicVar = 100; // OK! 모두 다 접근 가능.
  }
}

 

 

 

접근 범위 테스트 : protected 접근 II

 

new Exam0214() 하면 privateVar, defaultVar, protectedVar, publicVar 가 생성된다. Exam0214에서 protectedVar, publicVar에 접근 가능하다.

상속은 설계도 공유이다.

 

 

캡슐화 접근 범위 테스트 - 서브 클래스의 멤버가 접근할 수 있는 범위 II
package com.eomcs.oop.ex08.b;

public class Exam0214 extends com.eomcs.oop.ex08.b.sub.Y {

  public static void main(String[] args) {
    Exam0214 obj = new Exam0214();

    //    obj.privateVar = 100; // 접근 불가! 오직 그 클래스 안에서만 사용 가능.
    //    obj.defaultVar = 100; // 접근 불가! 같은 패키지까지만 접근 가능.

    obj.protectedVar = 100; // OK! Exam0214는 Y의 자식 클래스이며,
    // Y로부터 상속 받아서 만든 자기 변수이다.

    obj.publicVar = 100;
  }
}

 

 

캡슐화 접근 범위 테스트 - 종합
package com.eomcs.oop.ex08.b;

public class Exam0215 extends com.eomcs.oop.ex08.b.sub.Y {

  public static void main(String[] args) {
    m1(new X());
    m2(new com.eomcs.oop.ex08.b.sub.Y());
    m3(new Exam0215());
  }

  // 같은 패키지의 객체를 파라미터로 받은 경우
  static void m1(X obj) {
    //    obj.privateVar = 100; // 접근 불가!
    obj.defaultVar = 100; // OK! 같은 패키지
    obj.protectedVar = 100; // OK! 같은 패키지
    obj.publicVar = 100; // OK! 모두 접근 가능
  }

  // 다른 패키지의 객체를 파라미터로 받은 경우
  static void m2(com.eomcs.oop.ex08.b.sub.Y obj) {
    //    obj.privateVar = 100; // 접근 불가!
    //    obj.defaultVar = 100; // 접근 불가!
    //    obj.protectedVar = 100; // 접근 불가! Exam0215가 상속 받아 만든 변수가 아니다.
    obj.publicVar = 100; // OK! 모두 접근 가능
  }

  // 같은 객체를 파라미터로 받은 경우
  static void m3(com.eomcs.oop.ex08.b.Exam0215 obj) {
    // 다음 obj를 통해 접근하는 변수는
    // Exam0215 클래스의 인스턴스를 생성할 때 만든 변수이다.
    // 그러나 Exam0215 클래스에 선언된 변수가 아니라 상속 받은 변수이다.
    // 상속 받은 변수인 경우 상속 받은 클래스를 기준으로 접근 가능 여부를 따져야 한다.
    //    obj.privateVar = 100; // 접근 불가!
    //    obj.defaultVar = 100; // 접근 불가!
    obj.protectedVar = 100; // OK! Y 클래스를 상속 받아 만든 변수다.
    obj.publicVar = 100; // OK! 모두 접근 가능
  }

}

 

 

캡슐화(encapsulation) 응용 - 생성자를 private 으로 막기 1
package com.eomcs.oop.ex08.b;

class Car {
  String model;
  String maker;
  int cc;
  int valve;

  // 생성자를 private으로 선언하면 외부에서 이 클래스의 인스턴스를 생성하는 것을 막을 수 있다.
  private Car() {}

  // 예1) 인스턴스 생성과정이 복잡할 경우
  // - 직접 인스턴스를 생성하기 보다는
  //   인스턴스를 생성해주는 메서드를 사용하여 인스턴스를 만드는 것이
  //   유지보수할 때 편하다.
  // - 즉 인스턴스를 생성해 주는 메서드를 통해
  //   인스턴스를 생성하면 인스턴스를 사용하고픈 개발자는 코드가 간결해진다
  //   이런 설계 방식에 대해 이름을 붙였으니 
  //   그 이름도 유명한 "factory method" 설계 패턴이다.
  //
  public static Car create(String name) {

    Car c = new Car(); // private은 클래스 안에서 사용할 수 있다.

    switch (name) {
      case "티코":
        c.model = "티코";
        c.maker = "대우";
        c.cc = 800;
        c.valve = 16;
        break;
      case "소나타":
        c.model = "소나타";
        c.maker = "현대자동차";
        c.cc = 1980;
        c.valve = 16;
        break;
      default:
        c.model = "모델S";
        c.maker = "테슬라";
        c.cc = 0;
        c.valve = 0;
    }
    return c;
  }
}

public class Exam0221 {

  public static void main(String[] args) {

    //    Car c1 = new Car(); // 컴파일 오류!
    // 생성자가 private이기 때문에 다른 클래스에서 호출할 수 없다.
    // 따라서 인스턴스를 생성할 수 없다.
    //
    // 그럼 왜 생성자를 private으로 만들었는가?
    // => 개발자가 직접 인스턴스를 생성하면 너무 복잡하니,
    //    다른 메서드를 통해 인스턴스를 생성하라는 의미다!
    //
    Car c2 = Car.create("티코"); // 팩토리 일을 하는 메서드를 통해 인스턴스를 생성한다. 

    System.out.printf("%s,%s,%d,%d\n",
        c2.model, c2.maker, c2.cc, c2.valve);

  }
}

 

 

캡슐화(encapsulation) 응용 - 생성자를 private 으로 막기 2
package com.eomcs.oop.ex08.b;

class Car2 {
  String model;
  String maker;
  int cc;
  int valve;
}

class CarFactory {

  // 생성자를 private으로 선언하면 외부에서 이 클래스의 인스턴스를 생성하는 것을 막을 수 있다.
  private CarFactory() {}

  // 예2) 인스턴스를 오직 한 개만 생성해야 할 경우
  // - 인스턴스를 여러 개 생성할 필요가 없는 경우에 생성자를 private으로 막는다.
  // - getInstance() 같은  스태틱 메서드를 통해 인스턴스를 한 개만 만들어 사용한다.
  // - 이런 설계 기법을 "singleton" 패턴이라 부른다.
  //
  static CarFactory factory = null;
  public static CarFactory getInstance() {
    if (factory == null) {
      factory  = new CarFactory();
    }
    return factory;
  }

  // 다음은 CarFactory를 통해 자동차를 생성할 때 호출할 메서드이다.
  public Car2 create(String name) {

    Car2 c = new Car2();

    switch (name) {
      case "티코":
        c.model = "티코";
        c.maker = "대우";
        c.cc = 800;
        c.valve = 16;
        break;
      case "소나타":
        c.model = "소나타";
        c.maker = "현대자동차";
        c.cc = 1980;
        c.valve = 16;
        break;
      default:
        c.model = "모델S";
        c.maker = "테슬라";
        c.cc = 0;
        c.valve = 0;
    }
    return c;
  }
}

public class Exam0222 {

  public static void main(String[] args) {

    // 자동차 공장 객체를 먼저 만든다.
    //    CarFactory factory = new CarFactory(); // 컴파일 오류! 왜? 생성자가 private 이다.

    // 생성자가 private 일 경우 보통 스태틱 메서드를 통해 인스턴스를 생성한다.
    CarFactory factory = CarFactory.getInstance(); 

    Car2 c = factory.create("티코");

    System.out.printf("%s,%s,%d,%d\n",
        c.model, c.maker, c.cc, c.valve);

  }
}

Exam0221이나
  예) Car c2 = Car.create("티코");

Exam0222의 경우처럼,
  예) CarFactory factory = CarFactory.getInstance();

생성자가 private 접근으로 막혀 있어 
new 연산자를 이용하지 않고 스태틱 메서드를 호출해서 인스턴스를 생성하는 경우
다음 둘 중 하나다!
  1) 인스턴스 생성 과정이 복잡한 경우(Exam0221) 
  2) 인스턴스를 한 개만 생성해야 하는 경우(Exam0222)

 

 

캡슐화(encapsulation) 응용 - 생성자 접근이 금지된 또 다른 예
package com.eomcs.oop.ex08.b;

import java.util.Calendar;

public class Exam0230 {

  public static void main(String[] args) {
    // java.util.Calendar 객체 만들기
    // => 생성자가 protected로 되어 있다.
    // => 의미?
    //    - 보통 개발자가 클래스를 만들 때는 자신 만의 패키지에 넣어서 만든다.
    //    - java.util 패키지를 자신의 클래스를 두기 위해 사용하지는 않을 것이다.
    //    - 따라서 Calendar 생성자가 protected로 되어 있다는 것은
    //      개발자가 직접 생성자를 호출하지는 말라는 의미다.
    //      물론 Calendar를 만든 자신들은 나중에 Calendar의 서브 클래스를 만들 때
    //      이 생성자를 직접 사용하겠다는 의도로 생성자를 protected 했음을 알 수 있다.
    // => 그럼 이 클래스를 개발자가 사용하지 말라는 것인가?
    //    개발자들이 이 클래스의 인스턴스를 만들 수 있도록 스태틱 메서드를 제공한다.
    //    당연히 그 메서드는 외부에서 호출할 수 있도록 public으로 공개되었다.
    Calendar cal1 = Calendar.getInstance();
    Calendar cal2 = Calendar.getInstance();
    System.out.println(cal1 == cal2);

  }
}

getInstance()의 리턴 값을 확인해 보면 다르다는 것을 알 수 있다.
즉 getInstance() 메서드는 singleton 기능을 수행하는 메서드가 아니라,
복잡한 Calendar 객체를 대신 생성해주는 factory method로서 역할을 수행한다.

생성자를 protected로 감추고, getInstance()를 public 으로 공개하였다.
캡슐화를 응용한 예이다.

 

 

캡슐화(encapsulation) : 적용 전
package com.eomcs.oop.ex08.c;

class Patient {
  public static final int WOMAN = 1;
  public static final int MAN = 2;

  String name;
  int age;
  int height;
  int weight;
  int gender;

  @Override
  public String toString() {
    return String.format("name=%s, age=%d, height=%d, weight=%d, gender=%d",
        this.name, this.age, this.height, this.weight, this.gender);
  }
}

public class Exam0110 {

  public static void main(String[] args) {

    Patient p = new Patient();
    p.name = "김영희";
    p.age = 20;
    p.weight = 60;
    p.height = 157;
    p.gender = Patient.WOMAN;

    System.out.println(p);

    // 환자를 컴퓨터에서 다루려면 데이터화 해야 한다.
    // Patient는 이럴 목적으로 정의한 클래스이다.
    // 이렇게 Patient의 경우처럼 컴퓨터에서 다루기 위해 데이터화하여 정의하는 것을
    // "추상화"라 부른다.
    // 꼭 데이터만 해당하는 것은 아니다.
    // MemberHandler 클래스의 경우처럼 특정 업무를 정의하는 것도
    // "추상화"라 부른다.
    // => 즉 실세계의 객체(예: 사람, 물건, 업무, 개념 등)를 컴퓨터에서 다룰 수 있도록
    //    클래스로 정의하는 행위를 "추상화"라 부른다.
    Patient p2 = new Patient();
    p2.name = "이철희";
    p2.age = 300;
    p2.weight = -50;
    p2.height = 400;
    p2.gender = Patient.MAN;

    System.out.println(p2);

  }

}

위의 p2 인스턴스를 보면,
나이가 300이면 환자가 아니라 몬스터이다.
몸무게가 -50이면 이해 불가하다.
키가 4m 이면 나무다.
즉 Patient 클래스는 환자의 데이터를 저장할 목적으로 정의한 클래스인데
환자의 데이터와 무관한 몬스터 데이터를 저장하고 있다.
차라리 클래스 이름을 Monster로 변경하는 것이 바람직하다.
이렇게 클래스 목적에 맞지 않는 데이터가 들어 갈 수 있다면,
"추상화"가 무너지게 된다.
이를 방지하지 위해서는 클래스 목적(추상화 목적)에 맞춰
인스턴스 변수에 무효한 값이 들어가지 않도록 해야 한다.
그럴 목적으로 만든 문법이 캡슐화이다.

"캡슐화"? 추상화가 무너지지 않도록 인스턴스 멤버(변수와 메서드)의 접근을 제어하는 문법이다.
"추상화"? 실세계의 객체를 프로그램에서 다룰 수 있도록 클래스로 정의하는 것.
추상화 기법?
- 데이터 타입을 정의
- 유관 메서드를 묶기

 

 

캡슐화(encapsulation) : 적용 후
package com.eomcs.oop.ex08.c;

// 필드에 직접 접근하는 것을 막는다.
//
class Patient2 {
  public static final int WOMAN = 1;
  public static final int MAN = 2;

  private String name;
  private int age;
  private int height;
  private int weight;
  private int gender;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    if (age > 0 && age < 150)
      this.age = age;
  }

  public int getHeight() {
    return height;
  }

  public void setHeight(int height) {
    if (height > 0 && height < 300)
      this.height = height;
  }

  public int getWeight() {
    return weight;
  }

  public void setWeight(int weight) {
    if (weight > 0 && weight < 500)
      this.weight = weight;
  }

  public int getGender() {
    return gender;
  }

  public void setGender(int gender) {
    if (gender > 0 && gender < 3)
      this.gender = gender;
  }

  @Override
  public String toString() {
    return String.format("name=%s, age=%d, height=%d, weight=%d, gender=%d",
        this.name, this.age, this.height, this.weight, this.gender);
  }
}


public class Exam0120 {

  public static void main(String[] args) {


    Patient2 p = new Patient2();
    p.setName("김영희");
    p.setAge(20);
    p.setWeight(60);
    p.setHeight(157);
    p.setGender(Patient.WOMAN);

    System.out.println(p);

    Patient2 p2 = new Patient2();
    p2.setName("이철희");
    p2.setAge(300); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값은 무시된다.
    p2.setWeight(-50); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값은 무시된다.
    p2.setHeight(400); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값은 무시된다.
    p2.setGender(3); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값은 무시된다.

    System.out.println(p2);

  }

}

 

 

잘못된 값을 넣었을 때 무시하지 않고 강력하게 오류를 알리기!
package com.eomcs.oop.ex08.c;

class Patient3 {
  public static final int WOMAN = 1;
  public static final int MAN = 2;

  private String name;
  private int age;
  private int height;
  private int weight;
  private int gender;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    if (age > 0 && age < 150)
      this.age = age;
    else
      throw new RuntimeException("나이가 유효하지 않습니다!");
  }

  public int getHeight() {
    return height;
  }

  public void setHeight(int height) {
    if (height > 0 && height < 300)
      this.height = height;
    else
      throw new RuntimeException("키가 유효하지 않습니다!");
  }

  public int getWeight() {
    return weight;
  }

  public void setWeight(int weight) {
    if (weight > 0 && weight < 500)
      this.weight = weight;
    else
      throw new RuntimeException("몸무게가 유효하지 않습니다!");
  }

  public int getGender() {
    return gender;
  }

  public void setGender(int gender) {
    if (gender > 0 && gender < 3)
      this.gender = gender;
    else
      throw new RuntimeException("성별이 유효하지 않습니다!");
  }

  @Override
  public String toString() {
    return String.format("name=%s, age=%d, height=%d, weight=%d, gender=%d",
        this.name, this.age, this.height, this.weight, this.gender);
  }
}

public class Exam0130 {

  public static void main(String[] args) {


    Patient3 p = new Patient3();
    p.setName("김영희");
    p.setAge(20);
    p.setWeight(60);
    p.setHeight(157);
    p.setGender(Patient.WOMAN);

    System.out.println(p);

    Patient3 p2 = new Patient3();
    p2.setName("이철희");
    p2.setAge(300); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값을 넣으면 오류 발생!
    p2.setWeight(-50); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값을 넣으면 오류 발생!
    p2.setHeight(400); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값을 넣으면 오류 발생!
    p2.setGender(3); // 캡슐화를 무너뜨릴 수 있는 유효하지 않은 값을 넣으면 오류 발생!

    System.out.println(p2);

  }

}

 

 

 

 

Backend-app

 

myapp 프로젝트와 backend-app 프로젝트

 

그림 우측 backend-app 프로젝트는 아래부터 Java 기본 문법, OOP 문법, 기본 클래스를 쌓고 SpringBoot를 쌓아 올린다. 그 위에서 backend-app 올리고 App, HelloController를 올린다.

 

그림 좌측 myapp 프로젝트는 현재 Java 기본 문법, OOP 문법을 쌓고 위에 myapp을 올린 상태다. 추후 Tomcat Server를 직접  만들고 Servlet/JSP를 직접 만들고 Spring Framework를 직접 만든다. 그리고 이를 다시 SpringBoot로 대체하여 작동시킨다.

→ 기술 습득에 맞춰 프로젝트를 진화시킨다! → SpringBoot Application의 깊은 이해를 돕는다.

 

 

 

01. SpringBoot 프로젝트 준비

 

- Gradle 빌드 도구를 이용하여 프로젝트 폴더를 준비하는 방법

위와 같이 폴더 구성한다.

 

 

 

02. 게시글 관리 REST API 추가

 

App.java에 있던 hello()를 HelloController로 이동한다. App.java를 실행시키고 웹브라우저로 http://localhost:8080/hello 에 접속하면 Hello, world! 가 출력된다.

package bitcamp.bootapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin("*")
@SpringBootApplication
@RestController
public class App {

  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }

}
package bitcamp.bootapp;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// 다음 클래스가 클라이언트 요청을 처리하는 일을 한다는 것을 SpringBoot 에게 알리는 표시!
// => SpringBoot는 다음 클래스의 인스턴스를 생성해서 보관해 둔다.
// => "/hello" 라는 URL로 클라이언트 요청이 들어오면 해당 메서드를 호출한다.
@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    return "Hello, world!";
  }
}

 

Board.java를 복제해온다. BoardController를 아래와 같이 작성한다.

@애노테이션 문법은 SpringBoot에서 처리한다.

package bitcamp.bootapp;

public class Board {
  private int no;
  private String title;
  private String content;
  private String password;
  private String createdDate;
  private int viewCount;

  public int getNo() {
    return no;
  }
  public void setNo(int no) {
    this.no = no;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public String getCreatedDate() {
    return createdDate;
  }
  public void setCreatedDate(String createdDate) {
    this.createdDate = createdDate;
  }
  public int getViewCount() {
    return viewCount;
  }
  public void setViewCount(int viewCount) {
    this.viewCount = viewCount;
  }

}

 

package bitcamp.bootapp;

import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BoardController {
  static final int SIZE = 100;

  int count;
  Board[] boards = new Board[SIZE];
  String title;

  public BoardController() {
    Board b = new Board();
    b.setNo(1);
    b.setTitle("제목입니다.1");
    b.setContent("내용입니다.1");
    b.setPassword("1111");
    b.setCreatedDate("2023-1-1");
    b.setViewCount(1);

    this.boards[this.count++] = b;
  }

  @GetMapping("/boards/{boardNo}")
  public Object getBoard(@PathVariable int boardNo) {

    Board b = this.findByNo(boardNo);

    // 응답 결과를 담을 맵 객체 준비
    Map<String, Object> contentMap = new HashMap<>();

    if (b == null) {
      contentMap.put("status", "failure");
      contentMap.put("message", "해당 번호의 게시글이 없습니다.");
    } else {
      contentMap.put("status",  "success");
      contentMap.put("message", b);
    }

    return contentMap;
  }

  Board findByNo(int no) {
    for (int i = 0; i < this.count; i++) {
      if (this.boards[i].getNo() == no) {
        return this.boards[i];
      }
    }
    return null;
  }

}

 

http://localhost:8080/boards/1 에 접속시 json 형식으로 자료 보여준다.

{"message":{"no":1,"title":"제목입니다.1","content":"내용입니다.1","password":"1111","createdDate":"2023-1-1","viewCount":1},"status":"success"}

 

spring.io 에서 애노테이션 docs 확인 가능하다.

 

 

 

GRASP: High Cohesion (응집력을 높혀라)

 

how? → 1클래스 = 1책임

[myapp-11] 의 BoardHandler 클래스는 CLI I/O, Data 다루기 하였다. [backend-app-02] 의 BoardController 클래스에서 CLI I/O는 쓸모 없어 Web I/O 사용할 것이고 Data 다루기는 재활용한다.

Board 클래스는 통째로 재활용한다.

→ 처음부터 역할을 더 잘게 쪼갰더라면 일부 코드만 뜯어서 붙일 필요가 없었다.

→ 재활용성이 떨어지는 구조다.

→ 해결책!  1 클래스  = 1 책임(역할)  "High Cohesion"

 

 

 

CLI I/O vs HTTP I/O

 

CLI I/O는 제목을 "aaa" 입력하면 title 변수에 담아서 System.out.println() 으로 출력한다.

HTTP I/O는 Domain 주소로 값을 입력하고 이를 boardNo 변수에 담아서 client로 출력한다.

 

 

 

myapp

 

12. GRASP의 High cohesion 적용 : Data 처리 역할 분리

 

BoardHandler에서 UI 처리와 데이터 처리를 같이 하고 있다. 역할 분리하여 BoardDao에 게시글 데이터 처리를 맡긴다.

이유? 재사용성을 높힌다.

### 12. 인스턴스 목록을 다루는 코드를 분리: High Cohesion 구현(재사용성 강화) 
- GRASP의 OOP 원칙 중에서 High Cohesion을 구현한다. 
- BoardHandler의 역할에서 데이터 목록을 다루는 일을 BoardDao로 옮긴다.

 

package bitcamp.myapp;

import java.sql.Date;

public class BoardHandler {

  private BoardDao boardDao = new BoardDao();
  private String title;

  // 인스턴스를 만들 때 프롬프트 제목을 반드시 입력하도록 강제한다.
  BoardHandler(String title) {
    this.title = title;
  }

  void inputBoard() {
    Board b = new Board();
    b.setNo(Prompt.inputInt("번호? "));
    b.setTitle(Prompt.inputString("제목? "));
    b.setContent(Prompt.inputString("내용? "));
    b.setPassword(Prompt.inputString("암호? "));
    b.setCreatedDate(new Date(System.currentTimeMillis()).toString());

    this.boardDao.insert(b);
  }

  void printBoards() {
    System.out.println("번호\t제목\t작성일\t조회수");

    Board[] boards = this.boardDao.findAll();

    for (Board b : boards) {
      System.out.printf("%d\t%s\t%s\t%d\n",
          b.getNo(), b.getTitle(), b.getCreatedDate(), b.getViewCount());
    }
  }

  void printBoard() {
    int boardNo = Prompt.inputInt("게시글 번호? ");

    Board b = this.boardDao.findByNo(boardNo);

    if (b == null) {
      System.out.println("해당 번호의 게시글 없습니다.");
      return;
    }

    System.out.printf("    제목: %s\n", b.getTitle());
    System.out.printf("    내용: %s\n", b.getContent());
    System.out.printf("  등록일: %s\n", b.getCreatedDate());
    System.out.printf("  조회수: %d\n", b.getViewCount());
    b.setViewCount(b.getViewCount() + 1);
  }

  void modifyBoard() {
    int boardNo = Prompt.inputInt("게시글 번호? ");

    Board old = this.boardDao.findByNo(boardNo);

    if (old == null) {
      System.out.println("해당 번호의 게시글이 없습니다.");
      return;
    }

    // 변경할 데이터를 저장할 인스턴스 준비
    Board b = new Board();
    b.setNo(old.getNo());
    b.setCreatedDate(old.getCreatedDate());
    b.setTitle(Prompt.inputString(String.format("제목(%s)? ", old.getTitle())));
    b.setContent(Prompt.inputString(String.format("내용(%s)? ", old.getContent())));
    b.setPassword(Prompt.inputString("암호? "));

    if (!old.getPassword().equals(b.getPassword())) {
      System.out.println("암호가 맞지 않습니다!");
      return;
    }

    String str = Prompt.inputString("정말 변경하시겠습니까?(y/N) ");
    if (str.equalsIgnoreCase("Y")) {
      this.boardDao.update(b);
      System.out.println("변경했습니다.");
    } else {
      System.out.println("변경 취소했습니다.");
    }

  }

  void deleteBoard() {
    int boardNo = Prompt.inputInt("게시글 번호? ");

    Board b = this.boardDao.findByNo(boardNo);

    if (b == null) {
      System.out.println("해당 번호의 게시글이 없습니다.");
      return;
    }

    String password = Prompt.inputString("암호? ");
    if (!b.getPassword().equals(password)) {
      System.out.println("암호가 맞지 않습니다!");
      return;
    }

    String str = Prompt.inputString("정말 삭제하시겠습니까?(y/N) ");
    if (!str.equalsIgnoreCase("Y")) {
      System.out.println("삭제 취소했습니다.");
      return;
    }

    this.boardDao.delete(b);

    System.out.println("삭제했습니다.");

  }

  void searchBoard() {
    Board[] boards = this.boardDao.findAll();
    String keyword = Prompt.inputString("검색어? ");
    System.out.println("번호\t제목\t작성일\t조회수");
    for (Board b : boards) {
      if (b.getTitle().indexOf(keyword) != -1 ||
          b.getContent().indexOf(keyword) != -1) {
        System.out.printf("%d\t%s\t%s\t%d\n",
            b.getNo(), b.getTitle(), b.getCreatedDate(), b.getViewCount());
      }
    }
  }

  void service() {
    while (true) {
      System.out.printf("[%s]\n", this.title);
      System.out.println("1. 등록");
      System.out.println("2. 목록");
      System.out.println("3. 조회");
      System.out.println("4. 변경");
      System.out.println("5. 삭제");
      System.out.println("6. 검색");
      System.out.println("0. 이전");
      int menuNo = Prompt.inputInt(String.format("%s> ", this.title));

      switch (menuNo) {
        case 0: return;
        case 1: this.inputBoard(); break;
        case 2: this.printBoards(); break;
        case 3: this.printBoard(); break;
        case 4: this.modifyBoard(); break;
        case 5: this.deleteBoard(); break;
        case 6: this.searchBoard(); break;
        default:
          System.out.println("잘못된 메뉴 번호 입니다.");
      }
    }
  }
}

 

package bitcamp.myapp;

import java.util.Arrays;

public class BoardDao {
  private static final int SIZE = 100;

  private int count;
  private Board[] boards = new Board[SIZE];

  public void insert(Board board) {
    this.boards[this.count++] = board;
  }

  public Board[] findAll() {
    // 배열의 값 복제
    //    Board[] arr = new Board[this.count];
    //    for (int i = 0; i < this.count; i++) {
    //      arr[i] = this.boards[i];
    //    }
    //    return arr;

    // 위와 같다!
    return Arrays.copyOf(boards, count);
  }

  public Board findByNo(int no) {
    for (int i = 0; i < this.count; i++) {
      if (this.boards[i].getNo() == no) {
        return this.boards[i];
      }
    }
    return null;
  }

  public void update(Board board) {
    this.boards[this.indexOf(board)] = board;
  }

  public void delete(Board board) {
    for (int i = this.indexOf(board) + 1; i < this.count; i++) {
      this.boards[i - 1] = this.boards[i];
    }
    this.boards[--this.count] = null; // 레퍼런스 카운트를 줄인다.
  }

  private int indexOf(Board b) {
    for (int i = 0; i < this.count; i++) {
      if (this.boards[i].getNo() == b.getNo()) {
        return i;
      }
    }
    return -1;
  }
}

 

package bitcamp.myapp;

import java.sql.Date;

public class MemberHandler {

  MemberDao memberDao = new MemberDao();
  String title;

  MemberHandler(String title) {
    this.title = title;
  }

  void inputMember() {
    Member m = new Member();
    m.setNo(Prompt.inputInt("번호? "));
    m.setName(Prompt.inputString("이름? "));
    m.setTel(Prompt.inputString("전화? "));
    m.setPostNo(Prompt.inputString("우편번호? "));
    m.setBasicAddress(Prompt.inputString("주소1? "));
    m.setDetailAddress(Prompt.inputString("주소2? "));
    m.setWorking(Prompt.inputInt("0. 미취업\n1. 재직중\n재직자? ") == 1);
    m.setGender(Prompt.inputInt("0. 남자\n1. 여자\n성별? ") == 0 ? 'M' : 'W');
    m.setLevel((byte) Prompt.inputInt("0. 비전공자\n1. 준전공자\n2. 전공자\n전공? "));
    m.setCreatedDate(new Date(System.currentTimeMillis()).toString());

    this.memberDao.insert(m);
  }

  void printMembers() {

    Member[] members = this.memberDao.findAll();

    System.out.println("번호\t이름\t전화\t재직\t전공");

    for (Member m : members) {
      System.out.printf("%d\t%s\t%s\t%s\t%s\n",
          m.getNo(), m.getName(), m.getTel(),
          m.isWorking() ? "예" : "아니오",
              getLevelText(m.getLevel()));
    }
  }

  void printMember() {
    int memberNo = Prompt.inputInt("회원번호? ");

    Member m = this.memberDao.findByNo(memberNo);

    if (m == null) {
      System.out.println("해당 번호의 회원이 없습니다.");
      return;
    }

    System.out.printf("    이름: %s\n", m.getName());
    System.out.printf("    전화: %s\n", m.getTel());
    System.out.printf("우편번호: %s\n", m.getNo());
    System.out.printf("기본주소: %s\n", m.getBasicAddress());
    System.out.printf("상세주소: %s\n", m.getDetailAddress());
    System.out.printf("재직여부: %s\n", m.isWorking() ? "예" : "아니오");
    System.out.printf("    성별: %s\n", m.getGender() == 'M' ? "남자" : "여자");
    System.out.printf("    전공: %s\n", getLevelText(m.getLevel()));
    System.out.printf("  등록일: %s\n", m.getCreatedDate());
  }

  // 인스턴스 멤버(필드나 메서드)를 사용하지 않기 때문에
  // 그냥 스태틱 메서드로 두어라!
  static String getLevelText(int level) {
    switch (level) {
      case 0: return "비전공자";
      case 1: return "준전공자";
      default: return "전공자";
    }
  }

  void modifyMember() {
    int memberNo = Prompt.inputInt("회원번호? ");

    Member old = this.memberDao.findByNo(memberNo);

    if (old == null) {
      System.out.println("해당 번호의 회원이 없습니다.");
      return;
    }

    // 변경할 데이터를 저장할 인스턴스 준비
    Member m = new Member();
    m.setNo(old.getNo());
    m.setCreatedDate(old.getCreatedDate());
    m.setName(Prompt.inputString(String.format("이름(%s)? ", old.getName())));
    m.setTel(Prompt.inputString(String.format("전화(%s)? ", old.getTel())));
    m.setPostNo(Prompt.inputString(String.format("우편번호(%s)? ", old.getPostNo())));
    m.setBasicAddress(Prompt.inputString(String.format("기본주소(%s)? ", old.getBasicAddress())));
    m.setDetailAddress(Prompt.inputString(String.format("상세주소(%s)? ", old.getDetailAddress())));
    m.setWorking(Prompt.inputInt(String.format(
        "0. 미취업\n1. 재직중\n재직여부(%s)? ",
        old.isWorking() ? "재직중" : "미취업")) == 1);
    m.setGender(Prompt.inputInt(String.format(
        "0. 남자\n1. 여자\n성별(%s)? ",
        old.getGender() == 'M' ? "남자" : "여자")) == 0 ? 'M' : 'W');
    m.setLevel((byte) Prompt.inputInt(String.format(
        "0. 비전공자\n1. 준전공자\n2. 전공자\n전공(%s)? ",
        getLevelText(old.getLevel()))));

    String str = Prompt.inputString("정말 변경하시겠습니까?(y/N) ");
    if (str.equalsIgnoreCase("Y")) {
      this.memberDao.update(m);
      System.out.println("변경했습니다.");
    } else {
      System.out.println("변경 취소했습니다.");
    }

  }

  void deleteMember() {
    int memberNo = Prompt.inputInt("회원번호? ");

    Member m = this.memberDao.findByNo(memberNo);

    if (m == null) {
      System.out.println("해당 번호의 회원이 없습니다.");
      return;
    }

    String str = Prompt.inputString("정말 삭제하시겠습니까?(y/N) ");
    if (!str.equalsIgnoreCase("Y")) {
      System.out.println("삭제 취소했습니다.");
      return;
    }

    memberDao.delete(m);

    System.out.println("삭제했습니다.");

  }

  void searchMember() {

    Member[] members = this.memberDao.findAll();

    String name = Prompt.inputString("이름? ");

    System.out.println("번호\t이름\t전화\t재직\t전공");

    for (Member m : members) {
      if (m.getName().equalsIgnoreCase(name)) {
        System.out.printf("%d\t%s\t%s\t%s\t%s\n",
            m.getNo(), m.getName(), m.getTel(),
            m.isWorking() ? "예" : "아니오",
                getLevelText(m.getLevel()));
      }
    }
  }

  void service() {
    while (true) {
      System.out.printf("[%s]\n", this.title);
      System.out.println("1. 등록");
      System.out.println("2. 목록");
      System.out.println("3. 조회");
      System.out.println("4. 변경");
      System.out.println("5. 삭제");
      System.out.println("6. 검색");
      System.out.println("0. 이전");
      int menuNo = Prompt.inputInt(String.format("%s> ", this.title));

      switch (menuNo) {
        case 0: return;
        case 1: this.inputMember(); break;
        case 2: this.printMembers(); break;
        case 3: this.printMember(); break;
        case 4: this.modifyMember(); break;
        case 5: this.deleteMember(); break;
        case 6: this.searchMember(); break;
        default:
          System.out.println("잘못된 메뉴 번호 입니다.");
      }
    }
  }
}

 

package bitcamp.myapp;

import java.util.Arrays;

public class MemberDao {
  private static final int SIZE = 100;

  private int count;
  private Member[] members = new Member[SIZE];

  public void insert(Member member) {
    this.members[this.count++] = member;
  }

  public Member[] findAll() {
    return Arrays.copyOf(members, count);
  }

  public Member findByNo(int no) {
    for (int i = 0; i < this.count; i++) {
      if (this.members[i].getNo() == no) {
        return this.members[i];
      }
    }
    return null;
  }

  public void update(Member member) {
    this.members[this.indexOf(member)] = member;
  }

  public void delete(Member member) {
    for (int i = this.indexOf(member) + 1; i < this.count; i++) {
      this.members[i - 1] = this.members[i];
    }
    this.members[--this.count] = null; // 레퍼런스 카운트를 줄인다.
  }

  private int indexOf(Member b) {
    for (int i = 0; i < this.count; i++) {
      if (this.members[i].getNo() == b.getNo()) {
        return i;
      }
    }
    return -1;
  }
}

 

 

 

 

13. 패키지를 이용하여 클래스를 분류하는 방법

 

### 13. 패키지를 이용하여 클래스를 분류하는 방법 + 접근 제어 조정 
- 유지보수하기 좋게 클래스를 역할에 따라 분류한다.   
- 패키지 분류에 따라 멤버의 접근 범위를 조정한다.

 

 

 

 


 

 

조언

 

*

 

 

 


 

과제

 

myapp 복습

- app-10~13 진행 다시 하기