티스토리 뷰

Java

[Java의 정석] 6. 객체 지향 언어

rang-dev 2023. 4. 6. 17:47

1. 객체 지향 언어

객체지향언어의 역사

  • 과거: 모의 실험시 실제 세계와 유사한 가상 세계를 컴퓨터 속에 구현하려고 노력 → 객체 지향론 탄생
  • 객체지향론의 기본 개념
    • 실제 세계는 사물(객체)로 이루어져 있고 발생하는 모든 사건은 이들간의 상호 작용이다.
    • 실제 사물의 속성 → 데이터(변수), 사물의 기능 → 함수로 정의
  • 절차적 언어가 많이 사용되다가 프로그램의 규모 증가와 사용자들의 요구의 빠른 변화에 대응하기 위해서 객체 지향 언어의 입지가 넓어짐
  • 또한 인터넷의 발전은 객체 지향 언어를 프로그래밍의 주류로 만듬

객체지향언어

  • 객체 지향이란 완전히 새로운 것이 아닌 기존의 규칙에 몇가지 새로운 규칙이 추가된 것
    • 이러한 규칙으로 코드간에 서로 관계를 맺어주고 유기적으로 프로그램을 구성
  • 객체지향언어의 주요 특징
    • 코드의 재사용성
    • 유지보수 용이
      • 코드의 관계를 이용, 적은 노력으로 코드를 쉽게 변경 가능
    • 중복된 코드의 제거(신뢰성 높은 프로그래밍)
      • 제어자, 메소드 → 데이터 보호, 올바른 값 유지
      • 코드의 중복 제거 → 코드의 불일치로 인한 오동작 방지
  • 이러한 특징은 프로그램의 유지보수에 드는 비용을 획기적으로 개선
  • 처음부터 객체지향적으로 프로그래밍하려고 하지 말고, 기능적인 구현을 한 다음 코드를 객체 지향적으로 개선하려고 고민해보기

2. 클래스와 객체

클래스와 객체의 정의와 용도

  • 클래스
    • 객체를 정의해 놓은 것, 객체의 설계도
    • 용도: 객체를 생성하는데 사용
  • 객체
    • 실제 존재하는 사물 또는 개념(유, 무형 모두 가능)
      • 무형 예시 - 수학 공식, 프로그램 에러과 같은 논리나 개념
    • 클래스에 정의된 대로 메모리에 생성된 것 (프로그래밍 관점)
    • 용도: 객체의 속성과 기능에 따라 다름
  • 클래스는 TV 설계도, 객체는 TV에 비유
    • TV 설계도는 TV를 만드는데 사용된다.
    • TV 설계도가 있어야 TV를 만들 수 있다.
  • 하나의 설계도만 만들어놓으면 제품을 설계도대로만 만들면 됨 → 클래스를 사용하는 이유
  • JDK(Java Development Kit)에서 제공하는 유용한 클래스를 사용하여 원하는 프로그램을 보다 쉽게 작성 가능

객체와 인스턴스

  • 클래스의 인스턴스화: 클래스로부터 객체를 만드는 과정
  • 클래스의 인스턴스: 어떤 클래스로부터 만들어진 객체
  • 객체: 모든 인스턴스를 대표하는 포괄적인 의미 → 책상은 객체다.
  • 인스턴스: 어떤 클래스로부터 만들어진 것인지를 강조 → 책상은 책상 클래스의 인스턴스다.

객체의 구성요소 - 속성과 기능

  • 객체: 속성과 기능의 집합
  • 객체의 멤버: 객체가 가지고있는 속성과 기능
  • 클래스에는 이러한 속성과 기능이 미리 정의되어 있음
  • 속성과 기능의 동의어
    • 속성(property): 멤버 변수(member variable), 특성(attribute), 필드(field), 상태(state)
    • 기능(function): 메서드(method), 함수(function), 행위(behavior)
  • 객체 지향 프로그래밍: 속성 → 변수, 기능 → 메서드
Class Tv {
    String color;
    boolean power;
    int channel;

    void power ()     { power = !power }
    void chnnelUp ()     { channel++;}
    void channelDown ()     { channel--; }
}
  • 멤버변수는 멤버변수끼리 메서드는 메서드끼리 모아놓는 것이 일반적

인스턴스의 생성과 사용

  • TV 설계도만으로는 TV를 사용할 수 없음 → TV 인스턴스를 생성해야 함
Tv t;            // 클래스명 변수명;     ----- (1)
t = new Tv();    // 변수명 = new 클래스명(); ----- (2)
  1. Tv t;
  • 참조변수 t 선언
  • 메모리에 참조변수 t를 위한 공간이 마련 (내용은 없음)
  1. t = new Tv();
  • 연산자 new에 의해 Tv 클래스의 인스턴스가 메모리의 빈공간에 생성
    • 멤버변수는 각 자료형에 해당하는 기본값으로 초기화(ex. 참조형: null, boolean: false, int: 0)
  • 주소가 0x100인곳에 생성되었다면 대입 연산자에 의해 생성된 객체의 주소값이 참조변수 t에 저장
    • → 참조변수 t가 Tv 인스턴스 주소를 가리킴

⇒ 인스턴스는 참조변수를 통해서만 다룰 수 있으며 참조변수의 타입은 인스턴스의 타입과 일치해야 함!

class Tv  {
    String cololr;
    boolean power;
    int channel;

    void power() {power = !power;}
    void channelUp() {++channel;}
    void channelDown() {--channel;}
}

class TvTest3 {
    public static void main(String args[]) {
        Tv t1 = new Tv();
        Tv t2 = new Tv();

        System.out.println("t1의 channel값은 " + t1.channel + "입니다");
        System.out.println("t1의 channel값은 " + t2.channel + "입니다");

        t2 = t1;
        t1.channel = 7;

        System.out.println("t1의 channel값은 " + t1.channel + "입니다");
        System.out.println("t1의 channel값은 " + t2.channel + "입니다");
    }
}
  • t2 = t1; 에서 참조변수 t2는 t1의 인스턴스의 주소까지 가르키게 되기 때문에 t2 인스턴스는 참조되는 곳이 하나도 없어짐 → 더이상 사용될 수 없는 인스턴스 ⇒ 가비지 콜렉터에 의해 자동적으로 메모리에서 제거
  • 참조변수에는 하나의 값만 저장 가능 → 여러개의 인스턴스를 가르킬 수는 없음

객체배열

  • 많은 수의 객체를 다뤄야할 때 객체를 배열로 다룰 수 있음
    • 객체 배열은 참조 변수들을 하나로 묶은 참주 변수 배열
Tv[] tvArr = new Tv[3]     // 참조변수 배열 생성 -> 참조변수만 생성, 아직 객체는 저장되지 않음

// 객체를 생성하여 배열의 각 요소에 저장
tvArr[0] = new Tv();
tvArr[1] = new Tv();
tvArr[2] = new Tv();

// 배열 초기화 블럭 사용 가능
Tv[] tvArr = { new Tv(), new Tv(), new Tv() };

// 배열이 많을때는 for문을 사용할수도 있음
Tv[] tvArr = new Tv[100];

for(int i=0, i<tvArr.length;i++) {
    tvArr[i] = new Tv();
}
  • 객체 배열은 같은 타입의 객체만 저장 가능

프로그래밍 관점에서의 클래스의 정의와 의미

  • 객체지향적 관점의 클래스: 객체를 생성하기 위한 틀, 속성과 기능으로 정의됨

1. 데이터와 함수의 결합

  • 변수: 하나의 데이터를 저장
  • 배열: 같은 종류의 여러 데이터를 하나의 집합으로 저장
  • 구조체: 여러 종류의 여러 데이터를 하나의 집합으로 저장
  • 클래스: 데이터와 함수의 결합(구조체에 함수가 더해짐)
    • 예시로 자바에서는 문자열을 String이라는 클래스로 다룸
    • → 문자열 + 문자열을 다루는데 필요한 함수(문자열 뽑아내기, 문자열 길이 등)를 함께 묶기 위해서
    • 변수와 함수가 유기적으로 연결되어 작업이 간단하고 명료해짐

2. 사용자 정의 타입(user-defined type)

  • 언어에서 기본으로 제공하는 자료형 외에 프로그래머가 변수들을 묶어서 하나의 타입으로 새로 추가하는 타입

  • 자바에서는 클래스가 곧 사용자 정의 타입

  • ex. 여러개의 시간을 다뤄야할 경우

    • 위와 같은 코드의 한계
      • 다루는 시간이 추가될때마다 변수를 하나씩 추가해야함 ⇒ 배열로 해결 가능
    int[] hour = new int[3];
    int[] minute = new int[3];
    float[] second = new float[3];
    • 하지만 배열로 다루어도 시, 분, 초가 따로 존재하기 때문에 올바르지 않은 데이터가 될 가능성이 있음
    • ⇒ 클래스로 정의가 필요
    class Time {
        int hour;
        int minute;
        int second;
    }
    • 시, 분, 초가 하나의 단위로 묶임

    • but 제약조건이 필요(시: 024, 분&초: 060) → 제어자와 메소드로 쉽게 반영이 가능

      • 제어자로 변수의 값을 직접 변경하지 못하도록 하고, 메소드로만 변경할 수 있도록 함
      • 메소드로 변경시 유효한 값인지 유효성을 검사
      public class Time {
          private int hour;
          private int minute;
          private int second;
      
          public int getHour() { return hour; }
          public int getMinute() {return minute; }
          public int getSecond() { return second; }
      
          public void setHour(int h) {
              if (h<0 || h>23) return;
              hour = h;
      }
      
          public void setMinute(int m) {
              if (m<0 || m>59) return;
              minute = m;
      }
      
          public void setSecond(float s) {
              if (s<0.0f || s>59.99f) return;
              second = s;
      }

3. 변수와 메서드

선언 위치에 따른 변수의 종류

  • 변수의 종류를 결정 짓는 요소: 변수의 선언된 위치
    • 멤버변수
      • 선언 위치: 클래스 영역
      • static이 붙음: 클래스변수
      • static이 붙지 않음: 인스턴스 변수
    • 지역변수: 멤버변수를 제외한 나머지 변수
      • 선언 위치: 클래스 영역 이외의 영역(메서드, 생성자, 초기화 블럭 내부)
class Variables {
    int iv;             // 멤버변수, 인스턴스 변수
    static int cv;      // 멤버변수, 클래스 변수

    void method()
    {
        int lv = 0;     // 지역변수
    }
}

인스턴스 변수(instance variable)

  • 선언 위치: 클래스 영역
  • 생성 시기: 클래스의 인스턴스 생성시
  • 독립적인 저장 공간 소유 → 서로 다른 값을 가질 수 있음
    • 인스턴스마다 다른 값을 사용해야 하는 경우

클래스 변수(class variable)

  • 선언 위치: 클래스 영역, 인스턴스 변수 앞에 static을 붙임
  • 생성 시기: 클래스가 메모리에 올라갈때
  • 모든 인스턴스가 공통된 저장공간을 공유
    • 모든 인스턴스가 동일한 값을 공유해야하는 경우
  • 인스턴스를 생성하지 않고 바로 사용할 수 있음 (클래스명.변수명)
  • 프로그램이 종료될때까지 유지됨
  • public을 붙이면 프로그램 어디서나 접근 가능한 전역 변수로 사용됨

지역 변수(local variable)

  • 메서드 내에서 선언, 메서드 내에서만 사용 가능
  • 생성 시기: 변수 선언문이 수행되었을때
  • 메서드 종료시 소멸됨
  • 블럭({}) 내에서 선언된 경우에는 블록 내에서만 사용 가능, 블록을 벗어날 시 소멸됨

클래스 변수와 인스턴스 변수

  • 카드 클래스를 생성한다고 할때 공통적인 속성인 폭, 높이는 클래스변수, 카드의 개별적인 속성인 무늬, 숫자는 인스턴스 변수가 되어야 함

      class CardTest{
           public static void main(String args[]) {
               System.out.println("Card.width = " + Card.width);
               System.out.println("Card.height = " + Card.height);
    
               Card c1 = new Card();
               c1.kind = "Heart";
               c1.number = 7;
    
               Card c2 = new Card();
               c2.kind = "Spade";
               c2.number = 4;
    
               System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", "+ c1.height + ")");
               System.out.println("c2은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", "+ c1.height + ")");
    
               System.out.println("c1의 width, height를 각각 50, 80으로 변경합니다.");
               c1.width = 50;
               c1.height = 80;
    
               System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", "+ c1.height + ")");
               System.out.println("c1은 " + c1.kind + ", " + c1.number + "이며, 크기는 (" + c1.width + ", "+ c1.height + ")");
           }
      }
      class Card {
          String kind;
          int number;
    
          static int width = 100;
          static int height = 250;
      }
    • 클래스 변수를 사용할때 참조변수.클래스변수로도 사용할 수 있지만 클래스 변수를 인스턴스 변수로 착각하기 쉬우므로 클래스이름.클래스변수로 사용을 추천

메서드

  • 특정 작업을 하는 일련의 문장들을 하나로 묶은 것
  • 특정 입력값을 넣으면 작업을 수행 후 결과를 반환
  • 입력과 출력만 알고 구체적인 작업 수행 과정은 알지 않아도 됨

메서드를 사용하는 이유

  • 높은 재사용성(Reusability)

    • 한번 메서드를 만들어두면 여러번 호출이 가능하며 다른 프로그램에서도 호출하여 재사용이 가능
  • 중복된 코드의 제거

    • 같은 내용의 문장을 메서드를 호출하는 한 문장으로 대체

    • 전체 소스코드의 길이가 짧아짐

    • 변경사항이 발생했을때 수정할 코드가 줄어듦 → 관리 용이, 오류 발생 가능성도 낮아짐

        class MethodExam {
            static void printArr(int[] numArr){
                for (int i=0;i<10;i++) {
                    System.out.printf("%d", numArr[i]);
                    System.out.println();
                }
            }
      
            public static void main(String args[]) {
                int[] numArr = new int[10];
      
                for (int i=0;i<10;i++)
                    numArr[i] = (int) (Math.random()*10);
      
        //        for (int i=0;i<10;i++) {
        //            System.out.printf("%d", numArr[i]);
        //            System.out.println();
        //        }
                printArr(numArr);
      
            }
        }
    • 프로그램의 구조화

      • main안에 모든 문장을 넣는 방식 대신, 작업 단위로 나누어 메서드를 생성하여 프로그램의 구조를 단순화

          public static void main(String args[]) {
                  int[] numArr = new int[10];
        
                  initArr(numArr);
                  printArr(numArr);
                  sortArr(numArr);
                  printArr(numArr);
              }
          }
      • 이처럼 main에서는 전체 흐름을 한눈에 파악할 수 있도록 단순하게 구조화하는 것이 좋다.

        • 프로그램에 문제가 발생하면 원인을 빠르게 파악하여 수정이 가능
      • 처음 프로그램 설계시 메소드 별로 작업 단위를 만들어두고 작업을 하는 것도 구조화에 좋은 방법임

          static int showMenu() {}
              static void inputRecord() {}
              static void changeRecord() {}
              static void deleteRecord() {}
              static void searchRecord() {}
              static void showRecordList() {}
        
          public static void main(String args[]){
              switch (showMenu()) {
                  case 1: inputRecord(); break;
                  case 2:changeRecord(); break;
                  case 3: deleteRecord(); break;
                  case 4: searchRecord(); break;
                  default: showRecordList();
              }
          }

메서드의 선언부(method declaration, method header)

  • 메서드의 선언부: 반환타입 메서드이름 (타입 변수명, 타입 변수명, ...)
    • int add (int a, int b)
  • 선언부에 변경이 생기게되면 호출하는 부분을 모두 수정해야하기 때문에 신중히 작성해야한다.

매개변수 선언

  • 메서드가 작업을 수행하는데 필요한 값들
  • 두 변수 타입이 같아도 타입은 생략할 수 없다. → [X] int add (int x, y)
  • 입력해야할 값이 많다면 배열이나 참조변수를 이용

매서드의 이름

  • 동사 사용
  • 이름만 봐도 메서드의 기능을 유추할 수 있도록 함축적이면서 의미있는 이름

반환 타입

  • 작업 수행 결과인 반환 값의 타입
  • 반환값이 없는 경우 void로 설정

메서드의 구현부(method body)

  • 메서드 선언 이후 {} 안에 들어가는 내용
  • 메서드를 호출했을때 수행될 문장

return 문

  • 메서드의 반환 타입이 void가 아닌 경우에는 무조건 return 문이 들어가있어야 한다.
  • return으로 반환되는 값의 타입은 메서드 선언시 지정해주었던 반환 타입과 동일하거나 자동 형변환(작은 타입 → 큰 타입)이 가능해야한다.
  • return문은 단 하나의 값만 반환 가능하다.

지역변수(local variables)

  • 메서드 내에서 선언된 변수들은 지역변수라고 하며 함수 종료시 소멸되는 변수이기 때문에 서로 다른 메서드에서 동일한 변수명을 사용할 수 있다.
int add(int x, int y){
    int result = x + y;
    return result;
}

int multifly(int x, int y){
    int result = x * y;
    return result;
}

// 두 메소드의 x, y는 각각 이름만 같을 뿐 서로 다른 변수이다.

메서드의 호출

  • 메서드를 호출해야만 구현부의 코드들이 실행됨

인자와 매개변수(argument, parameter)

  • 인자: 메서드 호출시 괄호안에 지정한 값
  • 인자의 개수와 순서는 매개변수와 동일해야 함
  • 인자의 타입은 매개변수의 타입과 동일하거나 자동형변환 가능한 타입이어야 함
class MethodExam {
    static int add(int x, int y){       // 매개변수(parameter)
        int result = x + y;
        return result;
    }

    public static void main(String args[]) {  
        int result = add(1, 2);       // 인자(argument)
    }
}
  • 매개변수의 타입을 맞춰주지 않으면 다음과 같은 에러가 발생
    • add(1.0, 2) 호출시
      • java: incompatible types: possible lossy conversion from double to int
  • 반환 타입이 void가 아닌 경우에 반환된 값을 변수에 저장하는 것이 보통이지만 저장하지 않아도 된다.

메서드의 실행흐름

  • 같은 클래스내의 메서드끼리는 참조변수를 사용하지 않고 바로 호출 가능
    • 단, static 메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없음
class MyMathTest{
    public static void main(String args[]){
        MyMath mm = new MyMath();

        long result1 = mm.add(5L, 3L);
                // 1. 호출시 지정한 5L, 3L이 메서드의 매개변수에 각각 복사됨
                // 2. 메서드 호출부가 실행됨
                // 3. 호츨부가 모두 실행되거나 return문을 만나면 호출한 메서드(main)으로 돌아온 후 이후 문장들을 실행
                // 호출 결과가 바로 변수에 저장되는 것이 아니라 호출한 자리를 반환 값이 대신한 후 대입 연산자에 의해 변수에 저장됨
        long result2 = mm.subtract(5L, 3L);
        long result3 = mm.multiply(5L, 3L);
        double result4= mm.divide(5L, 3L);
                // long 타입을 인자로 넣어도 double형으로 자동형변환(작은타입 -> 큰타입)이 가능하므로 실행 가능
                // 연산 결과는 double형으로 나옴

        System.out.println("add(5L, 3L) = "+result1);
        System.out.println("subtract(5L, 3L) = "+result2);
        System.out.println("multiply(5L, 3L) = "+result3);
        System.out.println("divide(5L, 3L) = "+result4);
    }
}

class MyMath{
    long add(long a, long b){
        long result = a+b;
        return result;
        // return a+b 한줄로 가능
    }

    long subtract(long a, long b) {return a-b;}
    long multiply(long a, long b) {return a*b;}
    double divide(double a, double b) {return a/b;}
}
  • result
    add(5L, 3L) = 8
    subtract(5L, 3L) = 2
    multiply(5L, 3L) = 15
    divide(5L, 3L) = 1.6666666666666667

return문

  • return문: 현재 실행중인 메소드를 종료하고 호출한 메서드로 되돌아감
  • 원래는 반환값의 유무에 관계 없이 메서드에는 return문이 존재해야함
    • void인 경우에는 컴파일러에서 자동으로 return;을 추가하는 것
    • void가 아닌 경우, return문이 없으면 컴파일 에러가 발생
    • 조건문에 따라 return문이 호출되지 않는 경우가 발생할 수 있으므로 주의

반환값(return value)

  • 반환값으로 주로 변수가 오지만 수식이 올수도 있음

    • 수식이 아닌, 수식의 결과가 반환됨(return x+y;)

    • 단, 호출한 메서드의 반환 타입이 호출되는 메서드의 반환타입과 일치해야 함

        int diff(int x, int y) {
            return abs(x-y);
        }

매개변수의 유효성 검사

  • 구현부 작성시 매개변수의 값이 적절한지를 가장 먼저 판단해야함

  • 호출하는 쪽에서는 어떠한 값이나 넣을 수 있기 때문에 가능한 경우의 수를 고민하고 이에 대비한 코드를 작성해야함

      float divide(int x, int y){
          if(y==0) {
              System.out.println("0으로 나눌 수 없습니다.);
              return 0;
          }
          return x/(float)y;
      }
  • 적절하지 않은 값이 매개변수를 통해 넘어온 경우

    • 매개변수를 보정
    • 보정이 불가능한 경우라면 return문을 통해 작업을 중단 후 호출한 메서드로 되돌아감

'Java' 카테고리의 다른 글

[Java] Kafka Producer Java Client 실습해보기  (0) 2023.11.04
댓글