Welcome! Everything is fine.

[Java/Study] 김영한의 실전 자바 기본 - 스터디 3회차 본문

Java

[Java/Study] 김영한의 실전 자바 기본 - 스터디 3회차

개발곰발 2024. 10. 15.
728x90

 

인프런 강의 <김영한의 실전 자바 - 기본편>을 보고 정리한 내용입니다.

매주 모여 각자 정리한 내용을 기반으로 발표하고 질문 공유하는 스터디입니다.


 

생성자

생성자(Constructor)객체를 생성한 직후 초기화하기 위한 특별한 메서드이다. 생성자 대신 따로 메서드를 만들어서 초기화할 수도 있지만, 초기화하는 과정을 누락할 수도 있고 번거롭기 때문에 생성자를 사용한다.

 

생성자와 메서드 비교?

  • 클래스명과 같은 이름을 사용하기 때문에 대문자로 시작한다.
  • 생성자는 반환 타입이 없다.
  • 나머지는 메서드와 같다.
  • 생성자도 메서드 오버로딩처럼 여러개 정의할 수 있다. (여러개일 경우 하나만 호출하면 된다.)

다음은 생성자를 사용한 예시이다. 멤버 변수와 매개변수의 이름이 다르면  this를 생략할 수 있지만, 멤버 변수와 매개변수의 이름이 같으면 this를 사용해서 명확하게 구분할 수 있다. this를 붙이면 자기 자신의 인스턴스 참조값 가리킨다. 요즘은 IDE에서 색상으로 구분해줘서 이름이 다르면 굳이 적어주지 않아도 구분이 용이하다.

public class MemberConstruct {
    String name;
    int age;
    int grade;
    
    MemberConstruct(String name, int age, int grade) {
        System.out.println("생성자 호출 name= " + name + ", age= " + age + ", grade= " + grade);
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}

 

생성자는 인스턴스를 생성하고 나서 바로 호출된다. new 명령어 다음에 생성자 이름과 매개변수에 맞추어 인수를 전달하면 된다.

public class ConstructMain1 {
    public static void main(String[] args) {
        // 생성자는 인스턴스를 생성하고 나서 즉시 호출됨
        MemberConstruct member1 = new MemberConstruct("user1", 15, 90);
        MemberConstruct member2 = new MemberConstruct("user2", 16, 80);

        MemberConstruct[] members = {member1, member2};

        for (MemberConstruct s : members) {
            System.out.println("이름: " + s.name + " 나이: " + s.age + " 성적: " + s.grade);
        }
    }
}

 

결과는 다음과 같다.

생성자 호출 name=user1,age=15,grade=90
생성자 호출 name=user2,age=16,grade=80
이름:user1 나이:15 성적:90
이름:user2 나이:16 성적:80

 

생성자의 장점?

  • 중복 호출 제거 : 생성자를 사용하지 않으면 객체를 생성한 후 메서드를 따로 호출해야 한다. 하지만 생성자를 사용하면 객체를 생성하는 동시에 바로 초기화가 되기 때문에 더 편리하다. 
  • 제약 : 만약 생성자가 없는 경우 초기화하는 메서드를 호출하는 것을 까먹었다면 초기화가 되지 않은 채 프로그램이 실행될 수 있다. 하지만 직접 정의한 생성자가 존재한다면 반드시 호출해야하기 때문에(호출하지 않으면 컴파일 오류 발생) 필수값 입력을 보장할 수 있다.
좋은 프로그램은 무한한 자유도가 주어지는 프로그램이 아니라 적절한 제약이 있는 프로그램이다.

 

클래스에 생성자가 하나도 없으면 자바 컴파일러는 우리 눈에 보이지 않는 기본 생성자를 자동으로 만든다. 사용자가 생성자를 하나라도 만들면 자바는 기본생성자를 만들지 않는다.

 

자바에서 기본 생성자를 자동으로 만들어주는 이유?

기본 생성자를 만들어주지 않는다면 생성자가 필요없을 때도 개발자가 직접 기본 생성자를 정의해야한다. 생성자가 꼭 필요하지 않은 경우 편리하게 사용할 수 있도록 이러한 기능을 제공한다.

 

생성자는 다음과 같이 this()를 이용해서 메서드 오버로딩처럼 매개변수만 다르게 여러 생성자를 제공할 수 있다. this()생성자 코드의 첫줄에만 작성할 수 있다는 규칙이 있다. 해당 규칙을 어길 경우 컴파일 오류가 발생한다.

public class MemberConstruct {
    String name;
    int age;
    int grade;

    // 추가 - 생성자 오버로딩
    MemberConstruct(String name, int age) {
        this(name, age, 50); // 중복 제거 - 생성자 내부에서 자신의 생성자 호출
    }

    MemberConstruct(String name, int age, int grade) {
        System.out.println("생성자 호출 name= " + name + ", age= " + age + ", grade= " + grade);
        this.name = name;
        this.age = age;
        this.grade = grade;
    }
}

패키지

패키지(package)란 물건을 운송하기 위한 포장 용기나 묶음을 뜻하며, 자바에서 폴더와 같은 개념을 제공하는 것과 같다. 패키지 안에 관련된 Java 클래스들을 분류해 넣는다.아래 예시에서 밑줄 친 user, product, order가 패키지이다. 그 안에는 관련된 클래스를 넣었다.

  • user
    • User
    • UserManager
    • UserHistory
  • product
    • Product
    • ProductCatalog
    • ProductImage
  • order
    • Order
    • OrderService
    • OrderHistory

실습을 위해 아래와 같이 패키지를 생성했다. pack 패키지를 만들고 그 아래 a 패키지, b 패키지를 만들었다. 계층  구조로 보이지만 pack, pack.a, pack.b 패키지는 서로 완전히 다른 패키지이다. 따라서 각 패키지의 클래스에서 다른 패키지의 클래스를 사용할 때는 import를 해서 사용해야 한다.

  • pack
    • a
    • b

패키지를 사용하는 경우 항상 코드 첫 줄에 [package 패키지명]과 같이  패키지 이름을 적어주어야 한다. 실습할 때는 IntelliJ가 알아서 만들어줘서 내가 따로 적을 필요는 없었다.

package pack;

public class Data {
    public Data() {
        System.out.println("패키지 pack Data 생성");
    }
}

 

아래와 같이 패키지명을 일일이 적어 사용할 수 있지만 import를 사용해 패키지명을 생략하는 것이 편하다. 

package pack;

public class PackageMain1 {
    public static void main(String[] args) {
        Data data = new Data(); // Data 클래스도 같은 패키지에 있으므로 그냥 불러 사용 함.
        pack.a.User user = new pack.a.User(); // 다른 패키지에 있으면 패키지명 + 클래스명으로 적어 사용 함.
   
    }
}

 

또한 import 시점에 *(별)을 사용해 특정 패키지에 포함된 모든 클래스를 불러올 수 있다.

package pack;

//import pack.a.User; // import로 불러옴으로써 패키지명을 다 적지 않아도 됨.
//import pack.a.User2;

import pack.a.*; // 패키지에 있는 모든 클래스를 불러 사용 가능.

public class PackageMain2 {
    public static void main(String[] args) {
        Data data = new Data();
        User user = new User();
        User2 user2 = new User2();
    }
}

 

만약 클래스 이름이 같아도 패키지 명으로 구분해 사용할 수 있다.

package pack;

import pack.a.User;

public class PackageMain3 {
    public static void main(String[] args) {
        User userA = new User();
        // 다른 패키지에서 이름이 동일한 클래스를 부를 때, 하나는 풀네임을 다 적어야 함.
        pack.b.User userB = new pack.b.User();
    }
}

 

패키지 규칙은 다음과 같다.

  • 패키지 이름과 위치는 폴더 위치와 같아야 한다.
  • 패키지 이름은 모두 소문자를 사용한다.
  • 패키지 이름의 앞 부분에는 일반적으로 회사의 도메인 이름을 거꾸로 사용한다.

접근 제어자(access modifier)

접근 제어자(access modifier)해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용/제한할 수 있는 것을 말한다. 자바에서는 4가지 종류의 접근 제어자가 있으며 필드, 메서드, 생성자에 사용된다. 클래스 레벨에서도 일부 접근 제어자(public, default)를 사용할 수 있다.

  • private : 모든 외부 호출을 막는다.
  • default(package-private) : 같은 패키지 안에서의 호출은 허용한다.
  • protected : 같은 패키지 안에서의 호출은 허용하고, 패키지가 달라도 상속 관계의 호출은 허용한다.
  • public : 모든 외부 호출을 허용한다.
    • public 클래스는 반드시 파일명 == 클래스 이름!
    • 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있다.
    • 하나의 자바 파일에 default 클래스는 무한정 등장할 수 있다.

접근 제어자는 속성과 기능을 외부로부터 숨기기 위해 필요하다. 다음과 같이 음량이 100이 넘어가면 고장이 나는 스피커 객체가있다고 생각해보자.

public class Speaker {
    int volume;

    Speaker(int volume) {
        this.volume = volume;
    }

    void volumeUp() {
        if (volume >= 100) {
            System.out.println("volume은 100을 초과할 수 없습니다.");
        } else {
            volume += 10;
            System.out.println("volume 10 증가");
        }
    }

    void volumeDown() {
        volume -= 10;
        System.out.println("volume 10 감소");
    }

    void showVolume() {
        System.out.println("현재 volume : " + volume);
    }

}

 

만약 볼륨이 100을 초과하면 안된다는 사실을 잘 모르는 개발자가 새로 왔고, 스피커의 볼륨 필드에 직접 접근해서 수정할 수 있다면 볼륨 필드를 100을 넘도록 설정할 경우 스피커는 고장이 날 것이다. 따라서 volume 필드를 private으로 바꿔주면 스피커 안에서만 해당 필드에 접근할 수 있다.

private int volume;

 

따라서 volume 필드가 private일 때 외부에서 필드에 직접 접근하려고 한다면 컴파일 오류가 날 것이다. 이렇게 접근 제어자를 사용하면 데이터를 제대로 통제하고 관리할 수 있게된다.

Speaker speaker = new Speaker(90);
speaker.volume = 200; // private 접근 오류

 

객체 지향 프로그래밍의 중요한 개념 중 하나인 캡슐화에 대해 알아보자. 캡슐화란 데이터와 메서드를 하나의 단위인 클래스로 묶어 외부에서의 접근을 제어하는 것을 의미한다. 즉 데이터는 모두 숨기고 필요한 기능만 노출하는 것이 중요하다. 수업에 나온 예제를 하나 보자.

public class BankAccount {
    private int balance;

    public BankAccount() {
        balance = 0;
    }

    // public 메서드 : deposit
    public void deposit(int amount) {
        if (isAmountValid(amount)) {
            balance += amount;
        } else {
            System.out.println("유효하지 않은 금액입니다.");
        }
    }

    // public 메서드 : withdraw
    public void withdraw(int amount) {
        if (isAmountValid(amount) && balance - amount >= 0) {
            balance -= amount;
        } else {
            System.out.println("유효하지 않은 금액이거나 잔액이 부족합니다.");
        }
    }

    public int getBalance() {
       return balance;
    }

    // private 메서드 : isAmountValid
    private boolean isAmountValid(int amount) {
        return amount > 0;
    }
}

 

BankAccount 객체에서 숨겨진 것(private)과 노출된 것(public)을 나누면 다음과 같다. 데이터 필드와 금액 검증 기능은 숨기고 입금, 출금, 잔고 확인 기능과 같이 필수적인 기능만 노출하였다.

  • private : balance, isAmountVaild()
  • public : deposit(), withdraw(), getBalance()

사용자 입장에서는 3가지 기능만 알면 나머지는 모두 내부 객체에서 해주기 때문에 복잡하지 않고, 소중한 내 잔고 데이터도 보호할 수 있다. 만약 balance 필드를 외부에서 자유롭게 조절할 수 있다면 잔고를 무한정 늘려 출금하는 일이 벌어질 수도 있을 것이다.

public class BankAccountMain {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(10000);
        account.withdraw(20000);
        account.withdraw(3000);
        System.out.println("balance = " + account.getBalance());
    }
}

 

따라서 캡슐화와 접근 제어자를 통해서 할 수 있는 것은 다음과 같다.

  • 데이터를 안전하게 보호할 수 있다.
  • 사용할 때 복잡도를 낮출 수 있다.
  • 코드 재사용성 및 유지보수성을 증가시킬 수 있다.