Notice
Recent Posts
Recent Comments
05-18 01:37
«   2024/05   »
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 31
Archives
Today
Total
관리 메뉴

Byeol Lo

객체 지향 프로그래밍 Objective Oriented Programming (OOP) 본문

Programming Language/Java

객체 지향 프로그래밍 Objective Oriented Programming (OOP)

알 수 없는 사용자 2022. 10. 6. 01:12
 프로그램에 구현에 있어서 필요한 객체(부품)을 먼저 개발(생산)하고 이 객체(부품)들을 하나씩 조립해서 완성된 객체(부품)을 만드는 기법(방법)을 객체 지향 프로그래밍 이라고 한다.

 

객체(Object)

 물리적으로 존재하거나, 추상적 논리적으로 생각할 수 있는 것 중에서 자신의 속성(값)을 가지고 있고, 동작(함수)할 수 있는 것. 자바에서는 속성과 동작들을 각각 필드 field 와 메소드 method 라고 부르고,  현실 세계의 객체를 소프트웨어 객체로 설계하는 것을 객체 모델링 (Object Modeling) 이라고 한다.

 현실 세계는 모든 객체들이 서로 상호작용 하에 흘러가고 있다. 소프트웨어도 마찬가지이다. 객체들은 각각 독립적으로 존재하고, 다른 객체와 서로 상호작용 하면서 동작한다. 객체들 사이의 상호작용을 메소드로 정의하고, 객체가 다른 객체의 기능을 이용하는 것이 바로 메소드 호출이다. 메소드는 객체명.메소드명 으로 호출이 가능하다.

 객체 간의 관계에서 객체는 개별적으로 사용될 수 있지만, 대부분 다른 객체와 관계를 맺고 있다. 관계의 종류로는 집합 관계, 사용 관계, 상속 관계가 있다. 집합 관계에 있는 객체는 하나는 부품이고 하나는 완성품에 해당한다.

자동차 객체의 예

  • 집합 관계 : 하나의 완성품 객체에 대해서 다른 부속품 객체들을 만들고 그 객체들과 완성품 객체간의 관계
  • 사용 관계 : 객체 간의 상호작용을 하여 다른 객체의 메소드를 호출하여 원하는 결과를 얻어내는 관계
  • 상속 관계 : 상위(부모) 객체를 기반으로 하위(자식) 객체를 생성하는 관계. 보통 상위 객체는 종을 의미하고, 하위는 구체적인 사물에 해당한다.

 

OOP의 특징

  • 캡슐화(Encapsulation) : 객체의 필드, 메소드를 하나로 묶고, 실제 구현 내용을 감추는 것 ( 외부의 객체는 내부 객체 구조를 알 수 없음 ) 해당 캡슐화 기능을 이용하려면 접근 제어자(Access modifier)을 사용한다.
  • 상속(Inheritance) : 부모가 가지고 있는 필드와 메소드를 하위 객체에게 물려주어 하위 객체가 사용할 수 있도록 해준다.
  • 다형성(Polymorphism) : 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질을 말한다. 즉, 부모 타입에는 모든 자식 객체가 대입될 수 있고, 인터페이스 타입에는 모든 구현 객체가 대입될 수 있다.

 

객체Object와 클래스Class

 클래스는 객체를 만드는 설계도, 객체는 그 설계도를 통해 만들어지는 공장 생산품(?) 이라고 보면 된다. 클래스의 선언은 별다를거 없이 public 접근제한자를 뒀을때의 class는 파일당 한 개(이는 가독성을 위함) 이며, 다른건 개수 제한이 없다. 클래스 명에도 관례적인 것이 있는데 바로 단어의 첫번째는 대문자로 하며, 언더바 사용가능이다.

 클래스를 다 만들었다면, 이제 클래스로부터 객체를 생성시켜야 한다. 객체 생성은 다음 코드를 입력한다.

new ClassName();

 new 연산자로 생성된 객체는 heap 영역에서 객체를 생성시킨 후 객체의 주소를 리턴하도록 되고 있다. 따라서 Stack 영역에 변수를 선언하여 해당 변수가 heap의 객체를 가르키게 한다.

ClassName var1 = new ClassName();

 

객체 지향 개발 용도

다음 두 파일이 있다.

// Student.jaa
public class Student{

}
// StudentExample.java

public class StudentExample {
	pubilc static void main(String[] args) {
    	Student s1 = new Student();
        
    }
}

 각 두 파일은 용도가 있는데, 하나는 라이브러리 (API) 용이고 다른 하나는 실행용이다. 라이브러리 클래스는 다른 클래스에서 시용할 목적으로 설계가 된다. 프로그램에서 사용되는 클래스가 100개라면 99개는 라이브러리이고 단 하나가 실행 클래스다. 프로그램이 단 하나의 클래스로 구성되면 위의 파일을 하나로 합치면 좋겠지만, 대부분의 객체 지향 프로그램은 라이브러리와 실행 클래스가 분리되어 있다.

 

클래스의 구성 멤버

필드(Field)

 생성자와 메소드 전체에서 사용되며, 객체와 함께 존재한다. 객체의 고유 데이터, 객체가 가져야 할 부품, 객체의 현재 상태 데이터를 저장하는 곳이다.

class Person {

    Boolean thinkable;
    String sentiment;
    
    int age;
    
    Body body;
    Arm arm;
    Leg leg;
    Eye eye;
    //...
}

해당 변수들은 전부 고유 데이터 > 상태 > 부품 순으로 선언되고, 생성자와 메소드 중괄호 블록 내부에 선언된 것은 모두 로컬 변수가 된다. 필드 선언은 변수의 선언 형태와 비슷하다. (그래서 클래스 멤버 변수라고 부르기도 한다) 되도록 필드라는 용어를 그대로 사용하는 것이 좋다.

 초기값이 지정되지 않은 필드들은 객체 생성 시 자동으로 기본 초기값으로 설정된다. 이렇게 생성된 객체의 필드값들을 사용하려면 도트 연산자를 통해 객체에 접근할 수 있다.

생성자(Constructor)

 new로 객체 생성 시 초기화를 담당한다. 필드를 초기화 하거나 메소드를 호출해서 객체를 사용할 준비를 하는 것이고, 객체를 생성할 때 해당 코드를 실행하게 된다.

기본 생성자 : 모든 클래스는 생성자가 반드시 하나 이상 존재한다. 우리가 클래스 내부에 생성자 선언을 생략했다면 컴파일러는 중괄호 블록에 내용이 비어있는 기본 생성자(Default Constructor)를 바이트 코드에 자동으로 추가시킨다.

 클래스가 public class로 선언되면 기본 생성자에서도 public이 붙지만, 클래스가 public 없이 class로만 선언되면 기본 생성자에도 public이 붙지 않는다. 접근제한자 게시물로 가서 해당 내용을 더 자세히 볼 수 있다. 명시적으로 생성자를 선언하는 이유는 객체를 다양하게 초기화하기 위해서이다.

생성자는 다음과 같이 명시적으로 선언할 수 있다.

public class Main {
	
    String nation = "한국";
    String name
    String ssn;
    
    Main(String n, String s) {
    	name = n;
        ssn = s;
    }
}

  매개변수를 이렇게 생성자로 접근하면 나중에 코드를 봤을때 변수가 어디에 이용되었는지 찾기가 힘들므로, 가독성을 위해 this 라는 것을 사용한다.

public class Main {
	
    String nation = "한국";
    String name
    String ssn;
    
    public Main(String n, String s) {
    	this.name = n;
        this.ssn = s;
    }
}

 생성자 오버로딩이라는 것도 있는데 외부에서 제공하는 다양한 데이터들을 이용해서 객체를 초기화하려면 생성자도 다양화될 필요가 있다. 따라서 제공되는 생성자의 arguments에 따라 다양한 방법으로 객체를 생성할 수 있도록 한다.

public Person {
    
    public Person() {
    
    }
    public Person(boolean thinkable) {
    
    }
    public Person(int age) {
    
    }
}

 근데 이런 생성자 코드 블럭들이 많아질 경우 생성자간의 중복 코드가 발생할 수 있는데, 이를 this()를 통해 매개 변수의 수만 달리하고 필드 초기화 내용이 비슷한 생성자에서 이러한 현상을 많이 볼 수 있다.

메소드(Method)

 메소드는 필드를 읽고 수정하는 역할도 하지만, 다른 객체를 생성해서 다양한 기능을 수행하고, 객체 간의 데이터 전달의 수단으로 사용된다. 따라서 객체의 동작에 해당하며, 중괄호 블록의 모든 코드들이 일괄적으로 실행된다. 생성자와는 다르게 실행 후 어떤 값을 리턴할 수도 있다.

 메소드 선언은 선언부(리턴 타입, 메소드이름, 매개변수 선언)와 실행 블록으로 구성되고, 메소드 선언부를 메소드 시그니처라고도 한다. 리턴타입에는 메소드가 반환하는 값에 따른 데이터 형 예약어가 들어가는데, 만약 리턴되는 값이 없다면 void를 쓴다. void로 선언된 리턴타입의 메소드는 return을 아예 쓸 수 없는건 아니고, 값을 리턴 안할 뿐이다.

 메소드 이름은 변수와 비슷하게 숫자로 시작하면 안되고, 첫문자를 소문자로 작성한다. 서로 다른 단어가 혼합된 이름이면 뒤이어 오는 단어의 첫머리 글자는 대문자로 하는게 약속이다. 또한 매개변수를 선언할때, 매개변수의 수를 모를 경우가 있다. 이때는 다음과 같이 배열 타입으로 선언해준다.

int sum1(int[] values) {
	
}

 

 또한 생성자 오버로딩과 마찬가지로 메소드 오버로딩도 있다.

메소드 호출

 메소드 호출은 객체 내인지 아니면 외인지에 따라 다른데, 내부에서는 그냥 간단하게 이름으로 호출하면 되지만, 외부에서 호출할 경우에는 우선 클래스로부터 객체를 생성한 뒤, 참조 변수를 이용해서 메소드를 호출해야 한다.

 

정적 멤버와 static

 이때까지 public class 내부의 public static void main 메소드가 선언되는데 void의 의미를 알았으니 이제 static이라는 접근제한자를 살펴보자.

 정적(static)은 고정된 이라는 의미를 가지고 있다. 정적 멤버는 클래스에 고정된 멤버로서 객체를 생성하지 않고 사용할 수 있는 필드와 메소드를 의미한다. 이들은 각각 정적 필드, 정적 메소드라고 부르고 정적 멤버는 객체(인스턴스)에 소속된 멤버가 아니라 클래스에 소속된 멤버이기 때문에 클래스 멤버라고도 한다.

정적 멤버 선언을 해보자. 정적 필드와 정적 메소드를 선언하는 방법은 필드와 메소드 선언 시 static 키워드를 추가적으로 붙이면 된다. 다음은 예이다.

public class Main{

    static int i = 10;
    
    public static int main() {
    	
    }
}

정적 필드와 정적 메소드는 클래스에 고정된 멤버이므로 클래스 로더가 클래스(바이트 코드)를 로딩해서 메소드 메모리 영역에 적재할 때 클래스별로 관리된다. 따라서 클래스의 로딩이 끝나면 바로 사용할 수 있다.

필드를 선언할 때 인스턴스 필드로 선언할 것인지 아니면 정적 필드로 선언할 것인가의 판단 기준은 객체마다 가지고 있어야 할 데이터라면 인스턴스 필드로 선언하고, 객체마다 가지고 있을 필요가 없으면 공용적인 데이터라면 정적(static) 필드로 선언하는 것이 좋다.

public class Calculator {

    static double pi = 3.141592;
    
    static int plus(int x, int y) {
    	return x+y;
    }
    
    static int minus(int x, int y) {
    	return x-y;
    }
}

 

정적 멤버 사용

클래스가 메모리로 로딩되면 정적 멤버를 바로 사용할 수 있는데, 클래스 이름과 함께 도트(.) 연산자로 접근한다.

이때, 정적 요소는 클래스 이름으로 접근하는 것이 좋고, 왠만해서는 객체 참조 변수로 접근은 하지 말자.

 

정적 초기화 블록

자바는 정적 필드의 복잡한 초기화 작업을 위해서 정적 블록(static block)을 제공한다.

static {
	...
}

 정적 블록은 클래스가 메모리로 로딩될 때 자동적으로 실행되며, 클래스 내부에 여러 개가 선언되어도 상관이 없다. 또한 클래스가 메모리로 로딩될 때 선언된 순서대로 실행된다.

public class Television {
    static String company = "Samsung";
    static String model = "LCD";
    static String info;
    
    static {
    	info = company + "-" + model;
    }
}

 

정적메소드와 블록 선언시 주의할 점

 정적 메소드와 정적 블록을 선언할 때 주의할 점은 객체가 없어도 실행된다는 특징 때문에, 이들 내부에 인스턴스 필드나 인스턴스 메소드를 사용할 수 없다. 또한 객체 자신의 참조인 this 키워드도 사용이 불가능하다. 그래서 다음 코드는 컴파일 오류가 발생한다.

public class Main {
    int a;
    void a1(){...};
    
    static int b;
    static void b1(){...};
    
    static {
    	// a = 100;   => 오류
        // a1(); --> 오류
        
        b = 10;
        b2();
    }
    
    public static void main(String[] args) {
    	this.a = 10; // 오류
        this.a1();   // 오류
        
        b = 10;
        b2();
    }
}

 위의 정적 메소드와 정적 블록에서 인스턴스 멤버를 사용하고 싶다면 객체를 먼저 생성하고 참조 변수로 접근해야한다.

 

싱글톤(Singleton)

 가끔 전체적인 프로그램에서 단 하나의 객체만 만들고 싶을때, 해당 객체를 Singleton 이라고 한다. 이때 싱글톤 클래스 외부에서 new 연산자로 생성자를 호출할 수 없도록 해야한다. 이때 접근제한자로 private를 붙여주면 된다. (하지만, 클래스 내부에서는 new 연산자로 생성자 호출이 가능하다) 또한 정적 필드도 private 접근 제한자를 붙여 외부에서 필드값을 변경하지 못하도록 막는다.

 이때, 싱글톤의 객체를 얻을 수 있는 유일한 방법으로 정적 메소드인 getInstance()를 선언하는 것이다. 다음은 그 예제이다.

private class Main() {
    private int default = 10;
    
    private Main() {
    	System.out.println("hi");
    }
    
    static Main getInstance() {
    	return Main;
    }
}

 Singleton을 가져오려면 getInstance를 통해 가져온다.

public class MainExample {
    public static void main(String[] args) {
    
        Main m1 = Main.getInstance();
        Main m2 = Main.getInstance();
        
    }
}

두 변수는 같은 객체를 참조하고 있는 셈이다.

 

final 필드와 상수

 final의 의미는 최종적이라는 뜻이며, 초기값이 저장되면, 이것이 최종적인 값이 되어서 프로그램 실행 도중에 수정할 수 없다. 이때, 상수와 같은게 아니냐고 생각할 수 있는데, 다르다. final 필드는 객체마다 저장되고, 생성자의 매개값을 통해서 여러 가지 값을 가질 수 있기 때문이다. 하지만 Constant는 아니다. 객체마다 생성되지 않고 매개값에 따라 바뀌지 않는다. 따라서 Constant는 static이면서 final이어야지 상수이다. 상수 선언은 모두 대문자로 해주는 것이 관례이다.

 

패키지

 프로젝트 개발에 빠질래야 빠질 수 없는 기능이다. 우리는 클래스를 체계적으로 관리해야한다. 유사한 기능끼리 모아서 하나의 폴더에 java 클래스들을 저장하고, 그 폴더를 패키지라고 한다. 패키지는 단순히 파일 시스템의 폴더 기능만 하는 것이 아니라 클래스의 일부분이다. 패키지는 클래스를 유일하게 만들어주는 식별자 역할을 하기 때문이다. 클래스 이름이 동일하더라도 패키지가 다르면 다른 클래스로 인식하게 된다는 것이다. 다음은 패키지를 선언하는 방법이다.

package 상위패키지.하위패키지

public class ClassName {
	
}

 패키지 이름에도 지켜야할 몇 규칙이 있다. 숫자로 시작하면 안되고, _, $를 제외한 특수 문자를 사용해서는 안되며, java로 시작하면 안되고, 모두 소문자로 작성한다. 따라서 주로 대규모 프로젝트나 회사의 패키지를 이용해서 개발할 경우 회사들 간에 패키지가 중복되지 않도록 회사의 도메인 이름으로 패키지를 만든다.

 패키지 선언이 포함된 클래스를 명령 프롬프트에서 컴파일 할 경우, 단순히 java ~.java로 컴파일해서는 패키지 폴더가 생성되지 않고, -d옵션을 추가해서 패키지가 생성될 경로를 다음과 같이 지정해야한다.

java -d ~/Downloads ~.java
java -d ../bin ~.java
java -d . ~.java

 

import 문

 같은 패키지에 속하는 클래스들은 아무런 조건 없이 다른 클래스를 사용할 수 있다. 하지만, 다른 패키지에 속하는 클래스를 사용할때는 어떻게 해야할까? 두가지 방법이 있다. 첫 번째는 패키지와 클래스를 모두 기술해서 사용하는 것이다.

package com.mycompany;

public class Person {
    static com.samsung.Phone phone = new com.samsung.Phone();
}

이 경우 너무 타이핑 횟수가 많아 불편하다. 다른 방법은 import문을 사용하는 것이다.

package com.mycompany;

import com.samsung.Phone;

public class Main {
	Phone phone = new Phone();
}

*을 사용해 패키지 내의 모든 클래스를 부르는 것도 가능하다.

 

클래스에서 default, public 접근 제한자

 클래스를 선언할때, public이라는 접근제한자를 많이 봤을 것이다. public을 생략하면 어떻게 될까? default 접근제한자를 자동적으로 가지게 되는데, 같은 패키지 내에서는 아무런 제한 없이 사용할 수 있지만 다른 패키지에서는 사용할 수 없도록 제한한다.

 public은 그와는 반대로 다른 패키지에서도 사용할 수 있다. 즉 import 자체가 안된다.

 

생성자의 접근 제한

 객체를 생성하기 위해서는 new 연산자로 생성자를 호출해야 한다. 이때, 접근권한을 주어서 다른 곳에서 호출을 할 수 없게끔 할 수 있다. 클래스에서는 생성자를 선언하지 않으면, 컴파일러에 의해 자동적으로 기본 생성자가 추가되는데, 이 생성자의 접근 제한은 클래스의 접근제한과 동일하다. 

 public 예

// A.java
package com.project1.process1;

public class A {
	//필드
    A a1 = new A(true);
    A a2 = new A(1);
    A a3 = new A("문자열");
    
    //생성자
    public A(boolean b) {
    	
    }
    
    A(int b) { //default 생략 가능
    	
    }
    
    private A(String s) {
    	
    }
}
//B.java
package com.project1.process1;

public class B {
    A a1 = new A(true); // o
    A a2 = new A(1); // o
    A a3 = new A("문자열"); // x
}

동일 경로에서는 private 동작 불가

package com.project1.process2;

import com.project1.process1.*;

public class C {
	A a1 = new A(true); // o
    A a2 = new A(1); // x
    A a3 = new A("문자열"); // x
}

다른 패키지에서는 package까지 동작 불가

 protected는 default 접근제한자와 마찬가지이지만, 다른 패키지에 속한 클래스가 protected 클래스의 자식 클래스라면 생성자를 호출할 수 있다.

필드와 메소드의 접근 제한

 필드와 메소드를 선언할 때 고려해야 하는 것은 클래스 내부에서만 사용할 것인지, 패키지 내에서만 사용할 것인지, 아니면 다른 패키지에서도 사용할 수 있도록 할 것인지 결정해야 한다.

package com.project3.process1;

public class A {
    public int field1;
    int field2;
    private int field3;
    
    
    public A() {
    	field1 = 1;
        field2 = 1;
        field3 = 1;
        
        method1();
        method2();
        method3();
        
    }
    
    public void method1() {}
    void method2() {}
    private method3() {}
}

위의 코드는 전부 정상적으로 실행된다.

package com.project3.process1;

public class B {
    public B() {
        A a = new A();
    
    	a.field1 = 1; // o
        a.field2 = 1; // o
        a.field3 = 1; // x
        
        a.method1(); // o
        a.method2(); // o
        a.method3(); // x
    }
}
package com.project3.process2;

import com.project3.process1.*;

public class C {
    public C() {
        A a = new A();
    
    	a.field1 = 1; // o
        a.field2 = 1; // x
        a.field3 = 1; // x
        
        a.method1(); // o
        a.method2(); // x
        a.method3(); // x
    }
}

 

Getter, Setter 메소드

 일반적으로 클래스 내부의 데이터 예를 들면 Math.PI같은 데이터들은 접근하지 못하게 막는게 일반적이다. 왜냐하면 객체의 데이터를 외부에서 마음대로 읽고 써버리면 객체의 무결성이 깨질 수 있다. 따라서 데이터에 직접 접근하지 못하게 메소드를 통해 접근하게 한다. 이러한 메소드는 총 2개가 있는데, 하나는 필드를 수정하도록 하는 것이고, 하나는 조회하도록 하는 것이다. get필드명(), set필드명()으로 주로 한다. 이때 필드명은 첫번째가 대문자이다.(메소드 명명 원칙)

package com.project1.process1;

public class Main {
    private int num = 100;
    
    public void setNum(int n) {
    	
        if(n>100) {
        	this.num = 100;
        } elif(n<0) {
        	this.num = 0;
        } else {
        	this.num = n;
        }
    }
    
    public void getNum() {
    	return this.num;
    }
}

 

'Programming Language > Java' 카테고리의 다른 글

Junit5 사용법 알아보기  (0) 2022.11.05
Java - Inheritance 상속  (2) 2022.10.13
참조타입  (0) 2022.09.24
조건문과 반복문  (0) 2022.09.14
자바 연산자  (2) 2022.09.08
Comments