2022-01-10(월) 9주차 1일

Jeongyun Heo·2022년 1월 10일
0

파일 API 활용 : 상속 문법을 이용해 읽고 쓰기 기능 확장

상속을 이용하여 한 줄 단위로 읽고 쓰는 기능을 추가한다.

Spring Boot ← Spring WebMVC, Spring IoC(Inversion of Control) ← Servlet/JSP ← networking, thread ← I/O API (입출력 API)

기능 확장 - 1. 기존 코드 변경

90-MyList프로젝트1 / 47 페이지

oop.ex05.x1.test2

기존 코드에 계속 기능을 추가하는 경우
A 클래스
f1 f2 ← P1
f3 ← P2
f4 f5 ← P3
f1 f2 f3 f6 ← P4 : f4 f5 기능을 사용하지 않는다.
그렇다고 기존 코드를 삭제할 수 없다.
왜? P2와 P3는 사용하기 때문이다.

이게 잘못됐다는 게 아니라 단점이 있다는 거

since 1.5
자바에서도 1.5부터 추가한 메서드 있고 그럼

Deprecated ← 지울 수는 없어서

새 기능을 추가하기 위해 기존 코드를 변경하는 방식의 문제점:
1) 새 기능을 추가하거나 변경하면서 기존 코드를 손 대는 경우, 없던 오류가 발생할 수 있다.
2) 기존 코드에 계속 코드를 추가하다 보면 코드의 덩치가 커지고 복잡해져서 유지보수가 어려워진다.
3) 새로 추가한 기능이나 변경한 기능이 다른 프로젝트에서 필요 없을 때 기존 코드에서 제거하기 힘들다.
   왜? 이전에 만든 프로젝트에서 그 기능을 사용하고 있기 때문이다.
4) 기존 소스가 없으면 이 방식을 사용할 수 없다.

⟹ 이런 문제점을 해결하기 위해 나온 문법이 "상속(inheritance)"이다.

기능 확장 - 2. 기존 코드 복제 후 변경

90-MyList프로젝트1 / 48 페이지

oop.ex05.x1.test3

A 클래스
f1 f2 ← P1에서 사용

A 클래스 복제해서 A2 클래스 만듦
f3 기능 추가
f1 f2 f3 ← P2에서 사용

A2 클래스를 복제해서 A3 클래스 만듦
f4 f5 기능 추가
f1 f2 f3 f4 f5 ← P3에서 사용

A 클래스 복제해서 A4 클래스 만듦
f6 기능 추가
f1 f2 f6 ← P4에서 사용

원본 클래스에 버그 존재 → 복제한 모든 클래스에 버그 존재
원본 클래스의 기능 변경 → 복제한 모든 클래스의 기능 변경
⟹ 코드 수정 시 모든 클래스에 대해서도 코드를 수정해야 한다.

기존 코드를 복제한 후 기능을 추가하는 방식의 문제점:
1) 같은 코드가 여러 클래스에 중복된다.
   => 코드를 변경할 때 원본을 복제해서 만든 모든 소스도 변경해야 하는 번거로움이 있다.
   => 예1) 원본 클래스에서 버그가 발견되었을 때,
           그 클래스를 복제해서 만든 모든 클래스에도 버그가 존재한다.
           원본 클래스 뿐만 아니라 복제한 클래스 모두에 대해서도 코드를 수정해야 한다.
   => 예2) 기존 기능을 변경할 때,
           그 클래스를 복제해서 만든 모든 클래스의 코드도 변경해야 한다.

⟹ 이런 문제점을 해결하기 위해 나온 문법이 "상속(inheritance)"이다.

기능 확장 - 3. 기존 코드를 연결하여 기능 추가

90-MyList프로젝트1 / 49 페이지

oop.ex05.x1.test4

상속도 유지보수를 편하게 하려고 나옴

A 클래스
f1 f2 ← P1

A2 클래스 : A 클래스 연결(상속)
프로젝트가 필요한 기능만 추가
f3 (추가할 기능만 작성) ← P2

A3 클래스 : A2 클래스 연결(상속)
f4 f5 기능만 작성 ← P3

A4 클래스 : A2 클래스 연결(상속)
f6 기능만 작성 ← P4

기존 코드를 연결한 후 새 기능만 추가한다.
기존 코드를 손댈 필요가 없다.
새 기능만 고민하면 된다.
기존 기능을 변경하기 쉽다.
⟹ 즉, 기존 코드를 그대로 물려받고 새 기능을 추가
⟹ 상속(Inheritance) = 연결

A 클래스에 버그가 있으면 A 클래스만 변경하면 된다.

f1의 기능을 변경하면 A와 연결된 다른 클래스도 즉시 변경된 기능을 사용할 수 있다.

🔹 상속의 단점
중간의 서브 클래스에서 추가한 필요 없는 기능을 제거할 수 없다.
P4 프로젝트에서는 A2가 추가한 f3 기능이 필요 없다. 하지만 A3가 A2를 상속받았기 때문에 해당 기능이 존재한다.

상속(Inheritance)
1) 특징
   - 기존 코드를 재사용할 수 있다. 
     => 같은 기능을 수행하는 코드를 재작성할 필요가 없다. 
     => 코드 중복을 줄인다.
   - 기존 코드의 소스가 없어도 재사용할 수 있다.
     => 소스가 없는 기존 코드에 새 기능을 추가하거나 기존 기능을 변경하기 쉽다.
2) 단점
   - 중간의 서브 클래스가 추가한 기능을 임의적으로 제거할 수 없다.
   - 즉 서브 클래스를 계속 만들어가다 보면 필요 없는 기능이 계속 누적되는 문제가 발생한다.

⟹ 이런 문제를 해결하기 위해 "위임" 방식을 사용한다.

상속에 대한 오해

90-MyList프로젝트1 / 52 페이지

A 클래스
f1 f2

B 클래스
A 클래스 연결 ← 기존 코드를 그대로 가져온다고 착각한다.
f3 기능 추가

B 클래스는 A 클래스 코드를 갖고 있지 않다!!
B 클래스에는 f3만 있는 거

B obj = new B();
obj.f3() ← B의 f3() 호출
obj.f2() ← A의 f2() 호출
obj.f1() ← A의 f1() 호출

찾는다. 없으면 따라 올라간다.
연결된 클래스로 따라 올라가면서 찾는다.

상속 문법과 용어

90-MyList프로젝트1 / 53 페이지

상속 문법 → 기능 확장 → 기존 기능을 그대로 가져오면서 새 기능을 추가하거나 기능 변경을 쉽게 할 수 있다.

상속이라는 문법을 사용하면 기존 코드를 손대지 않고 기능 확장을 할 수 있다.

Calculator
plus()
minus()

Calculator2
multiple() 추가

Calculator3
divide()

Calculator 기준
Calculator2 ← sub class, child class
Calculator3 ← sub class, child class

Calculator2 기준
Calculator ← super class, parent class

자바의 상속 계층도

java.lang.Object ← root class (최상위 클래스)
  ↑              ↑
String     java.util.Date
                 ↑
           java.sql.Date

자바의 모든 클래스는 Object의 sub 클래스다.

package com.eomcs.oop.ex05.x1.test4;

public class Calculator2 extends com.eomcs.oop.ex05.x1.test1.Calculator {
  public void multiple(int value) {
    this.result += value;
  }
}

Calculator 클래스에 있는 코드를 마치 이 클래스의 코드인 것처럼 사용하겠다는 의미

상속은 소스 파일이 필요 없다.

🔹 상속의 단점
중간의 서브 클래스에서 추가된 필요 없는 기능을 제거할 수 없다.
P4 프로젝트에서는 A2가 추가한 f3 기능이 필요 없다. 하지만 A3가 A2를 상속받았기 때문에 해당 기능이 존재한다.

2) 단점
‐ 중간의 서브 클래스가 추가한 기능을 임의적으로 제거할 수 없다.
‐ 즉 서브 클래스를 계속 만들어가다 보면 필요 없는 기능이 계속 누적되는 문제가 발생한다.

이런 문제를 해결하기 위해 "위임" 방식을 사용한다.

상속과 인스턴스 생성

90-MyList프로젝트1 / 54, 55 페이지

수퍼 클래스의 설계도에 따라 만든 변수

기존 코드를 그대로 가져온다고 착각한다.

Calculator.class 삭제하고 test4 실행해보기

Calculator.class 없어서 못 찾아서 에러

에러가 떴을 때 맨 아래를 봐야 됨

기존 코드를 사용하는 권한이 있다는 거

상속 받은 클래스 파일도 존재해야 됨
존재하지 않으면 에러 뜸

스페이스 한 칸 추가하고 저장하면 클래스 파일 다시 생김

1단계 - FileReader 클래스를 상속받아 FileReader2 클래스를 정의한다.

1단계 - FileReader 클래스를 상속받아 FileReader2 클래스를 정의한다.

한 줄 단위로 읽는 기능을 추가
readLine() 메서드 추가

read() ← java.io.InputStreamReader

파일 API 활용 : 상속으로 기능 확장

Object

Reader

InputStreamReader <---- read()

FileReader

FileReader2 <---- readLine()

그 중 하나라도 객체 생성 실패하면 멈춤

메서드 호출 스택을 거꾸로 보여주는 거

else if (c == '\r') {
  // 무시! CR(Carrage Return; \r) 코드는 버퍼에 담지 말고 버린다.
}

리눅스가 유닉스를 모방

Windows OS : OAOD (2 byte)
Linux/Unix : OA (1 byte)

2단계 - 페이지 컨트롤러 객체가 생성될 때 파일에서 한 줄의 데이터를 읽는 기능을 추가한다.

2단계 - 페이지 컨트롤러 객체가 생성될 때 파일에서 한 줄의 데이터를 읽어 객체로 로딩한다.

super() 메소드
http://www.tcpschool.com/java/java_inheritance_super

super() 메소드
this() 메소드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면, super() 메소드는 부모 클래스의 생성자를 호출할 때 사용됩니다.

자식 클래스의 생성자에서 부모 클래스의 생성자 호출

package com.eomcs.io;

// 기존 코드를 자신의 코드인양 사용하겠다고 선언한다.
public class FileReader2 extends java.io.FileReader {

  // 수퍼 클래스의 생성자는 바로 사용할 수 없다.
  // 서브 클래스의 생성자를 통해 사용해야 한다.
  public FileReader2(String filename) throws Exception {

    // 다음과 같이 수퍼 클래스의 생성자를 호출해야 한다.
    super(filename);
  }

  public String readLine() throws Exception {
    StringBuilder buf = new StringBuilder();

    int c;
    while ((c = this.read()) != -1) { // 파일에서 한 문자를 읽는다. 더 이상 읽을 문자가 없으면 반복문을 종료한다.
      if (c == '\n') { // 만약 읽은 문자가 줄바꿈 명령이라면, 지금까지 버퍼에 저장한 문자를 리턴한다.
        return buf.toString();
      } else if (c == '\r') {
        // 무시! CR(Carrage Return; \r) 코드는 버퍼에 담지 말고 버린다.
      } else { // 문자를 읽을 때마다 버퍼에 임시 보관한다.        
        buf.append((char)c);
      }
    }
    return buf.toString();
  }  
}
  public ContactController() throws Exception {
    contactList = new ArrayList();
    System.out.println("ContactController() 호출됨!");

    com.eomcs.io.FileReader2 in = new com.eomcs.io.FileReader2("contacts.csv");

    String line;
    while ((line = in.readLine()).length() != 0) { // 빈 줄을 리턴 받았으면 읽기를 종료한다.
      contactList.add(Contact.valueOf(line)); // 파일에서 읽은 한 줄의 CSV 데이터로 객체를 만든 후 목록에 등록한다.
    } 

    in.close();
  }

기존 코드 손 안 대고

이게 상속

상속과 인스턴스 생성

자기 클래스에 있는 인스턴스 변수 만들기 전에 수퍼 클래스 변수 먼저 만듦. 제일 위까지 올라가서 거기서부터 변수 만듦. 마지막에 자기 클래스에 있는 변수 만듦.

중복 코드 제거됨

같은 코드가 여러 곳에서 중복 작성되어 있다면 변경 사항이 발생할 때 중복된 곳을 모두 찾아 변경해야 하는 번거로움이 있다.

3단계 - FileWriter 기능을 확장한다.

데이터 저장 요청을 받았을 때 한 줄씩 출력하는 가능을 추가한다.

\n ← OA : line feed, new line

데코레이터 : 상속의 단점을 해결하기 위해서 사용하는 설계 기법

package com.eomcs.io;

import java.io.FileWriter;

// 상속 문법을 사용하여 기존의 FileWriter 클래스를 연결한다. 
// 그런 후 마치 자식의 코드인양 사용한다.
//
public class FileWriter2 extends FileWriter {

  // 이 클래스의 생성자를 통해 FileWriter 클래스의 생성자를 호출한다.
  public FileWriter2(String filename) throws Exception {
    super(filename);
  }

  // 한 줄 단위로 출력하는 기능을 추가한다.
  public void println(String str) throws Exception {
    this.write(str + "\n");
  }
}
  @RequestMapping("/contact/save")
  public Object save() throws Exception {
    FileWriter2 out = new FileWriter2("contacts.csv"); // 따로 경로를 지정하지 않으면 파일은 프로젝트 폴더에 파일이 생성된다.

    Object[] arr = contactList.toArray();
    for (Object obj : arr) {
      Contact contact = (Contact) obj;
      out.println(contact.toCsvString());
    }

    out.close();
    return arr.length;
  }

FileWriter 대신 FileWriter2 클래스를 이용하여 데이터를 출력한다.

기능 확장 - 4. 기존 코드를 연결하여 기능 추가 (위임 기법을 활용)

oop.ex05.x1.test5

포함을 활용

포함 관계를 이용한 기능 확장
상속에 비해 유연한 방식

파일 API 활용 : 포함 관계를 이용해 읽고 쓰기 기능 확장

생성자가 호출될 때 준비한 in

위임을 이용한 기능 수행

Client : 어떤 기능을 이용하는 쪽을 클라이언트라 부른다.

Client --①call--> Proxy --②call--> Original
Client <--④return-- Proxy <--③return-- Original

Client : ContactController
Proxy : FileReader2 readLine() 읽은 데이터를 가공하여 한 줄의 문자열을 리턴한다. Data Processing Steam 클래스 (데이터를 가공)
Original : FileReader read() 데이터를 읽는 일을 한다. Data Sink Stream 클래스 (데이터를 읽는 일)

package com.eomcs.oop.ex05.x1.test5;

import com.eomcs.oop.ex05.x1.test1.Calculator;

public class Calculator2 {
  // plus(), minus()는 기존의 Calculator 클래스에게 위임한다.
  Calculator origin = new Calculator();

  public void plus(int value) {
    // 이 클래스가 포함하고 있는 객체에게 실행을 위임한다.
    origin.plus(value);
  }

  public void minus(int value) {
    // 이 기능은 기존의 클래스가 처리하도록 기존 객체에게 위임한다.
    origin.minus(value);
  }

  // 새 기능 또한 기존 객체의 필드를 사용하여 처리한다.
  public void multiple(int value) {
    origin.result *= value;
  }

  public int getResult() {
    return origin.result;
  }
}
위임(delegation)
- 상속은 문법적으로 기능을 확장하기 때문에 코드가 경직되어 있다.
- 코드가 경직되어 있다?
  => 다음과 같이 클래스가 계층을 이루고 있다고 가정한다.
      A <-- B <-- C
     즉 B는 A를 상속받고, C는 B를 상속받는다.
  => C의 기능이 필요한 D는 다음과 같이 C를 상속 받을 것이다.
     C <-- D
  => 여기서 D는 B의 기능이 필요 없음에도 어쩔 수 없이 상속받아야 한다.
     왜? C가 B를 상속받기 때문이다.
     이것이 코드가 경직되었다는 뜻이다.
     필요 없는 기능을 자유롭게 뺄 수 없다. 유연성이 부족하다.

상속이 훨씬 단순해
그럼에도 불구하고 필요없는 기능도 상속해야 됨. 유연성이 부족.

위임도 단점이 있음.

각자 장단점이 있음.

Calculator2는 위임보다는 상속이 훨씬 나음

상속이 적합한 부분이 있고
위임이 적합한 부분이 있음

FileReader는 위임이 적합
read를 직접 안 쓰니까
위임하는 방식이 훨씬 더 코드를 유연하게

기존 코드를 그대로 가져오면서
기존 기능을 재사용하자는 거
처음부터 다시 짜지 말고
기존 코드를 버리지 말고 재사용
좋은 설계라는 것은 기존 코드를 버리지 않고 재사용하는 거

기존 코드를 가져오고 거기에 코드를 덧붙이는 거

상속이라는 방법이 있고 위임이라는 방법이 있다

위임 문법이라고 안 하고 기법이라고 한다.

기존 기능을 확장하는 설계 기법이구나

package com.eomcs.io;

import java.io.FileWriter;

// 위임 기법을 활용하여 기존 기능 확장하기
//
public class FileWriter2 {

  // 실제 파일로 데이터 출력을 실행할 객체를 포함한다.
  FileWriter out;

  // 이 클래스의 생성자를 통해 FileWriter 클래스를 준비한다.
  public FileWriter2(String filename) throws Exception {
    out = new FileWriter(filename);
  }

  // 한 줄 단위로 출력하는 기능을 추가한다.
  public void println(String str) throws Exception {
    // 실제 파일로 데이터를 출력하는 일은 FileWriter 객체에게 위임한다.
    out.write(str + "\n");
  }

  // 자원을 해제시키는 일은 실제 파일 출력을 수행하는 객체에게 넘긴다.
  public void close() throws Exception {
    out.close();
  }
}

기존 클래스 재사용
기존 코드를 재사용하는 멋진 문법
위임이라는 기법을 이용해서도 가능

기존에 작성한 코드를 재사용
재사용하기 쉬운 문법이 좋은 문법
언제 상속받아야 되고 언제 위임을 받아야 되고
디자인 패턴

0개의 댓글