본문 바로가기

Development/Java

Java에서 접근 제한자의 의미와 종류

 

 

접근 제한자의 의미

 

 

백엔드 개발자에게 있어 보안은 가장 중대한 이슈 중 하나이다. 소프트웨어 서버는 고객의 개인정보와 기밀 자료를 다루므로, 백엔드 개발자가 보안에 가장 많은 공을 들여야 하는 것은 당연지사다. 사회에서 간간히 발생하는 정보 유출 사태는 고객의 개인 정보나 기밀 자료가 유출되면 어떤 일이 발생하는지에 대한 좋은 예시를 보여 주었다.

 

고객 정보뿐만 아니라 기업의 자산인 비즈니스 로직을 노출하지 않는 것 역시 매우 중요한 일이다. 이를 위해 대부분의 소프트웨어 기업에서는 기업 용역의 핵심인 비즈니스 로직을 철저히 감추고, 외부에 소프트웨어의 기능을 공유하더라도 Open API를 통해 외부와 연결 가능한 공용 인터페이스만을 노출한다.

 

소프트웨어는 접근 제한자를 통해 외부에 노출되어도 무관한 일부 클래스 또는 클래스 내부의 일부 필드, 생성자, 메소드만을 노출할 수 있다. 이는 캡슐화를 통해 내부 데이터와 로직을 보호하는 역할을 할 수 있을 뿐만 아니라, 소프트웨어의 내부 컴포넌트 간 결합도를 낮춰 각 컴포넌트가 자신의 역할에만 충실할 수 있게 해 준다. 따라서 접근 제한자를 제대로 이해하고 올바르게 사용하는 것은 개발자에게 있어 기본적인 역량이라고 할 수 있다.

 

 

접근 제한자의 종류

 

 

Java에서 접근 제한자는 총 네 가지(public, protected, default, private)로 구성되어 있다. 이 중 protected는 다소 특수한 제한 방식을 가지기 때문에 사용 빈도가 높지는 않은 편이다.

 

예를 들어, interior 패키지에 다음과 같이 Person 클래스를 선언할 수 있다.

package interior;

public class Person {

    private String name;
    private int age;
    private String phoneNumber;
    private int weight;

    public Person() {

        name = "홍길동";
        age = 10;
        phoneNumber = "010-1234-5678";
        weight = 70;

    }

    public String getName() {
        return name;
    }

    protected int getAge() {
        return age;
    }

    String getPhoneNumber() {
        return phoneNumber;
    }

    private int getWeight() {
        return weight;
    }

}

 

모든 필드는 private, 생성자와 getName() 메소드는 public, getAge() 메소드는 protected, getPhoneNumber() 메소드는 default, getWeight() 메소드는 private으로 선언되어 있다. 여기서 default는 기본값으로서, 접근 제한자를 따로 명기하지 않으면 자동으로 default 접근 제한을 가지게 된다.

 

 

Public

 

 

public은 가장 공개적인 접근 제한자로서, 외부 어느 곳에서든 해당 요소에 접근, 사용할 수 있게 해 준다. 주로 외부 컴포넌트와의 연락 지점이자 해당 컴포넌트의 의사소통이 가능한 기능 명세를 공유하는 역할을 하는 interface나, 객체를 생성하고 초기화하는 생성자, 단순히 데이터를 조회하는 역할을 하는 getter, (일부 객체의 경우) 데이터를 입력할 수 있는 setter에서 가지게 된다.

 

다만 소프트웨어 외부에서 사용하고자 한다면 제약을 완전히 제거할 수는 없는데, 소프트웨어 외부에서 public 요소를 import하기 위해서는 JRE(Java Runtime Environment)가 요소에 접근하기 위한 경로(Classpath)를 확보할 수 있도록 해당 라이브러리가 추가되어야 하고, 이를 사용하려는 언어는 Java와 호환이 가능해야 한다.

 

다음 예시 코드는 Person 클래스가 속한 interior 패키지에서 Person 객체를 생성하고, 이름을 질문했다.

package interior;

public class Question {

    public String inquireName() {

        Person person = new Person();
        
        return person.getName();

    }

}

 

public 제한을 가진 생성자와 getName()이 정상적으로 호출되어, Person의 이름인 홍길동이 잘 출력된다.

package main;

import interior.Question;

public class Main {

    public static void main(String[] args) {

        Question question = new Question();

        System.out.println(question.inquireName()); // 홍길동

    }

}

 

이번에는 다른 패키지인 exterior에서 Person 객체를 생성하고, 이름을 질문했다.

package exterior;

import interior.Person;

public class Question {

    public String inquireName() {

        Person person = new Person();

        return person.getName();

    }

}

 

역시 정상적으로 홍길동이 출력된다.

package main;

import exterior.Question;

public class Main {

    public static void main(String[] args) {

        Question question = new Question();

        System.out.println(question.inquireName()); // 홍길동

    }

}

 

이처럼 public 접근 제한자는 패키지 내·외부에 관계없이 원하는 요소를 호출할 수 있게 해 준다.

 

 

Protected

 

 

protected는 public에 비해 보안이 강화된 접근 제한자이다. 호출 클래스의 호출 대상 클래스 상속 여부에 따라 접근 가능한 범위가 변화한다. 기본적으로는 패키지 내부에서만 호출이 가능하지만, 호출 대상 클래스를 상속받는 경우 외부 패키지에서도 해당 클래스의 protected 요소를 상속받아 사용할 수 있다.

 

public과 protected 제한자는 접근 제한이 상당히 느슨하므로, 반드시 필요한 상황에서만 사용하는 것이 추천된다. 그렇지 않으면 내부 로직 노출이나 잘못된 접근으로 인한 의도치 않은 시스템 변경 등의 문제가 발생할 소지가 높다.

 

Person과 같은 패키지인 interior 패키지에서 protected 접근 제한을 가진 나이 질문 메소드를 호출했다.

package interior;

public class Question {

    public int inquireAge() {

        Person person = new Person();

        return person.getAge();

    }

}

 

길동이의 나이 10살이 잘 출력된다.

package main;

import interior.Question;

public class Main {

    public static void main(String[] args) {

        Question question = new Question();

        System.out.println(question.inquireAge()); // 10

    }

}

 

이번에는 다른 패키지인 exterior에서 나이를 물어봤다.

package exterior;

import interior.Person;

public class Question {

    public String inquireAge() {

        Person person = new Person();

        return person.getAge(); // Compile error

    }

}

 

protected 접근 제한 때문에 Compile error가 발생했다.

 

이번에는 Question 클래스에서 Person 클래스를 상속한 뒤 호출해 봤다.

package exterior;

import interior.Person;

public class Question extends Person {

    public int inquireAge() {

        Person person = new Person();

        return person.getAge(); // Compile error

    }

}

 

여전히 Compile error가 발생한다. 이는 Question 클래스가 Person 클래스를 상속했지만, Person 객체를 생성해 호출했기 때문이다. 이 경우 다른 객체와 동등한 입장에서 메소드를 호출하게 된다. 상속의 효과를 누리려면, 부모 객체의 메소드가 아니라 부모 클래스에게 상속받은 자신의 메소드를 호출해야 한다. 따라서, 새로운 객체를 생성하지 않고 아래와 같이 호출해야 한다.

package exterior;

import interior.Person;

public class Question extends Person {

    public int inquireAge() {

        return getAge();

    }

}

 

이제 아래와 같이 나이가 잘 출력된다.

package main;

import exterior.Question;

public class Main {

    public static void main(String[] args) {

        Question question = new Question();

        System.out.println(question.inquireAge()); // 10

    }

}

 

 

Default

 

 

default 접근 제한은 package-private이라고도 하며, 이름처럼 동일한 패키지에 속한 객체의 요소만을 호출할 수 있게 해 준다. 따라서 외부 패키지의 객체는 상속을 받는다 하더라도 해당 요소를 호출할 수 없다.

 

default 접근 제한을 통해 외부로의 로직 노출 방지뿐만 아니라, 컴포넌트 간 분리를 통해 캡슐화를 달성할 수 있다. 앞에서 언급한 것처럼, 접근 제한자를 따로 명기하지 않으면 자동으로 접근 제한이 default로 설정된다.

 

Person 객체가 속한 interior 패키지에서 default 접근 제한을 가진 전화번호 질문 메소드를 호출했다(점점 질문의 난이도가 상승하고 있다).

package interior;

public class Question {

    public String inquirePhoneNumber() {

        Person person = new Person();

        return person.getPhoneNumber();

    }

}

 

아래와 같이 전화번호를 알아내는 데 성공했다.

package main;

import interior.Question;

public class Main {

    public static void main(String[] args) {

        Question question = new Question();

        System.out.println(question.inquirePhoneNumber()); // 010-1234-5678

    }

}

 

이번에는 다른 패키지인 exterior에서 나이를 물어봤다.

package exterior;

import interior.Person;

public class Question {

    public String inquirePhoneNumber() {

        Person person = new Person();

        return person.getPhoneNumber(); // Compile error

    }

}

 

패키지가 다르므로, 컴파일 에러가 발생한다.

 

이번에는 Question 객체에서 Person 객체를 상속한 뒤 호출해 봤다.

package exterior;

import interior.Person;

public class Question extends Person {

    public String inquirePhoneNumber() {

        return getPhoneNumber(); // Compile error

    }

}

 

protected가 아닌 default 제한을 가지므로, Person 객체를 상속해도 여전히 작동하지 않는다.

 

 

Private

 

 

private 접근 제한자는 모든 패키지에서의 접근을 원천 차단하고, 같은 클래스 내부에서만 호출할 수 있게 하는 아주 엄격한 제한자이다. 따라서 객체지향 프로그래밍의 핵심인 캡슐화를 통해 외부에서의 내부 로직 간섭을 방지하고, 컴포넌트 간 철저한 역할 분담을 실현할 수 있게 해 준다.

 

Person 객체가 속한 interior 패키지에서 private 접근 제한을 가진 몸무게 질문 메소드를 호출했다(현실의 난이도와 비례한다).

package interior;

public class Question {

    public int inquireWeight() {

        Person person = new Person();

        return person.getWeight(); // Compile error

    }

}

 

같은 패키지 내부에서도 실패했다. 따라서 외부 패키지에서는 당연히 불가능할 것이다. private 제한을 가진 요소는 아래와 같이 클래스 내부에서만 호출이 가능하다.

package interior;

public class Person {

    private int weight = 70;

    private int getWeight() {
        return weight;
    }

    public int inquireWeight() {

        Person person = new Person();

        return person.getWeight();

    }

}

 

이제 길동이의 몸무게를 확인할 수 있다.

package main;

import interior.Person;

public class Main {

    public static void main(String[] args) {

        Person person = new Person();

        System.out.println(person.inquireWeight()); // 70

    }

}

 

참고 자료
https://songacoding.tistory.com/92
 

[자바] 접근제한자 default vs protected 차이 (예시)

안녕하세요, 송아지할때 송아 김송아입니다. 자바의 접근제한자 4개 중 ✨default와 protected의 차이✨에 대해 예시를 통해 알아보겠습니다! default와 protected의 개념에 대해 이미 잘 알고 계신다고

songacoding.tistory.com