안녕하세요~ 경제적 자유입니다.
더 자세히 공부하고 싶다면 아래 문서도 함께 참고해보세요.
- 참고 링크: https://www.programiz.com/java-programming/access-modifiers
- 참고 링크: https://www.programiz.com/java-programming/this-keyword
- 참고 링크: https://www.programiz.com/java-programming/final-keyword
1. 접근 제한자
접근 제한자는 클래스, 필드, 생성자, 메서드에 누가 접근할 수 있는지를 정하는 문법입니다.
Java에서도 모든 필드와 메서드를 아무 곳에서나 사용할 수 있게 두지 않고, 필요한 범위만 공개할 수 있습니다.
이렇게 접근 범위를 조절하면 객체 내부의 값이 외부에서 함부로 변경되는 것을 막을 수 있습니다.
Java의 접근 수준
Java의 접근 수준은 다음과 같이 나뉩니다.
접근 수준접근 가능한 범위
| public | 어디에서나 접근 가능 |
| protected | 같은 패키지 또는 상속받은 자식 클래스에서 접근 가능 |
| package-private | 같은 패키지 안에서 접근 가능 |
| private | 선언된 클래스 내부에서만 접근 가능 |
여기서 주의할 점은 package-private에는 별도의 키워드가 없다는 것입니다.
아무 접근 제한자도 작성하지 않았을 때 package-private 접근 수준이 적용됩니다.
흔히 이를 “default 접근 제한자”라고 부르기도 하지만, 코드에 default라고 작성하지는 않습니다.
기본 문법
class Person {
public String publicName;
protected String protectedName;
String packageName; // package-private
private String privateName;
}
각 필드의 의미는 다음과 같습니다.
- publicName은 다른 클래스에서도 접근할 수 있습니다.
- protectedName은 같은 패키지 또는 자식 클래스에서 접근할 수 있습니다.
- packageName은 같은 패키지 안에서만 접근할 수 있습니다.
- privateName은 Person 클래스 내부에서만 접근할 수 있습니다.
private 필드와 getter
객체의 필드는 보통 private으로 선언하는 경우가 많습니다.
외부 코드가 필드에 직접 접근하지 못하게 막고, 필요한 경우 메서드를 통해 값을 읽거나 변경하게 만드는 방식입니다.
class Person {
private String name;
public Person(String name) {
this.name = name;
}
// 외부에서 이름을 읽을 수 있도록 공개한 메서드
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
System.out.println(person.getName());
// person.name처럼 직접 접근할 수는 없음
}
}
// 실행 결과:
// 상구
name 필드는 private이므로 Person 클래스 밖에서는 직접 접근할 수 없습니다.
대신 public으로 선언된 getName() 메서드를 통해 이름을 가져옵니다.
이렇게 객체 내부의 데이터를 숨기고, 정해진 방법으로만 접근하게 만드는 것을 캡슐화라고 합니다.
캡슐화는 단순히 값을 숨기는 데서 끝나는 것이 아니라, 잘못된 값이 들어오는 것을 막거나 객체의 상태를 안전하게 관리하는 데도 사용됩니다.
setter를 사용할 때
값을 변경해야 한다면 setter 메서드를 만들 수 있습니다.
다만 setter를 만든다는 것은 외부에서 값을 변경할 수 있도록 허용한다는 뜻이므로, 모든 필드에 무조건 만드는 것은 좋지 않습니다.
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void setName(String name) {
if (name == null || name.isBlank()) {
System.out.println("이름은 비워둘 수 없습니다.");
return;
}
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
person.setName("상구 수정");
System.out.println(person.getName());
}
}
// 실행 결과:
// 상구 수정
필드를 public으로 공개했다면 어떤 문자열이든 바로 넣을 수 있습니다.
하지만 setter를 사용하면 이름이 비어 있는지 검사한 뒤 값을 변경하는 식으로 객체의 상태를 관리할 수 있습니다.
2. this 키워드
this는 현재 실행 중인 객체 자신을 가리키는 참조입니다.
예를 들어 Person 객체가 자신의 이름을 저장하거나 자신의 메서드를 호출할 때, 그 객체 자신을 가리키는 표현이 this입니다.
특히 생성자나 setter에서 매개변수 이름과 필드 이름이 같을 때 자주 사용합니다.
기본 문법
class Person {
private String name;
Person(String name) {
this.name = name;
}
}
위 코드에는 이름이 같은 두 개의 name이 있습니다.
this.name = name;
왼쪽의 this.name은 현재 객체가 가지고 있는 인스턴스 변수입니다.
오른쪽의 name은 생성자로 전달받은 매개변수입니다.
따라서 이 코드는 다음과 같은 뜻입니다.
전달받은 name 값을 현재 객체의 name 필드에 저장한다.
this를 사용하지 않으면 어떻게 될까?
class Person {
private String name;
Person(String name) {
name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
System.out.println(person.getName());
}
}
// 실행 결과:
// null
name = name;이라고만 작성하면 두 name 모두 가까운 범위에 있는 매개변수를 의미합니다.
결국 매개변수 값을 다시 자기 자신에게 넣을 뿐, 객체의 필드에는 값이 저장되지 않습니다.
String 타입의 인스턴스 변수는 값이 저장되지 않으면 기본값인 null을 가지므로 출력 결과도 null이 됩니다.
이 문제를 해결하려면 필드 앞에 this를 붙여야 합니다.
예제
class Person {
private String name;
public Person(String name) {
// 현재 객체의 name 필드에 매개변수 name을 저장
this.name = name;
}
public void introduce() {
System.out.println("제 이름은 " + this.name + "입니다.");
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
person.introduce();
}
}
// 실행 결과:
// 제 이름은 상구입니다.
new Person("상구")가 실행되면 새로운 Person 객체가 만들어집니다.
그 객체의 생성자 안에서 this는 방금 만들어진 객체를 가리킵니다.
따라서 this.name = name;은 새 객체의 name 필드에 "상구"를 저장하는 코드입니다.
this로 현재 객체의 메서드 호출하기
this는 필드뿐 아니라 현재 객체의 다른 메서드를 호출할 때도 사용할 수 있습니다.
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void greet() {
System.out.println("안녕하세요.");
}
public void introduce() {
this.greet();
System.out.println("저는 " + this.name + "입니다.");
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
person.introduce();
}
}
// 실행 결과:
// 안녕하세요.
// 저는 상구입니다.
this.greet()는 현재 객체가 가지고 있는 greet() 메서드를 호출한다는 뜻입니다.
같은 클래스 안에서는 this를 생략하고 greet()라고 작성해도 동작합니다.
따라서 꼭 필요한 문법은 아니지만, 현재 객체의 필드나 메서드를 사용한다는 점을 분명하게 나타낼 수 있습니다.
this()로 다른 생성자 호출하기
this와 this()는 모습이 비슷하지만 역할이 다릅니다.
- this는 현재 객체를 가리킵니다.
- this()는 같은 클래스의 다른 생성자를 호출합니다.
class Person {
private String name;
private int age;
public Person() {
// 매개변수 두 개를 받는 생성자 호출
this("상구", 0);
}
public Person(String name) {
// 매개변수 두 개를 받는 생성자 호출
this(name, 0);
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void introduce() {
System.out.println("이름: " + name);
System.out.println("나이: " + age);
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
person.introduce();
}
}
// 실행 결과:
// 이름: 상구
// 나이: 0
Person(String name) 생성자는 this(name, 0)을 통해 매개변수 두 개를 받는 생성자를 호출합니다.
이렇게 하면 각 생성자에서 필드에 값을 넣는 코드를 반복해서 작성하지 않아도 됩니다.
다른 생성자를 호출하는 this()는 생성자의 첫 번째 문장에 있어야 합니다.
public Person(String name) {
System.out.println("생성 시작");
this(name, 0); // 컴파일 오류
}
this()보다 앞에 다른 실행문이 있으면 컴파일 오류가 발생합니다.
3. final 키워드
final은 더 이상 변경하거나 확장하지 못하도록 제한할 때 사용하는 키워드입니다.
- 변수에 붙으면 재할당할 수 없습니다.
- 메서드에 붙으면 자식 클래스에서 오버라이딩할 수 없습니다.
- 클래스에 붙으면 다른 클래스가 상속할 수 없습니다.
final을 단순히 “상수로 만드는 키워드”라고만 외우면 메서드와 클래스에서의 역할을 놓칠 수 있습니다.
final 변수
변수에 final을 붙이면 값을 한 번 할당한 뒤 다른 값으로 다시 할당할 수 없습니다.
기본 문법
final 자료형 변수명 = 값;
예를 들면 다음과 같습니다.
final String NAME = "상구";
관례상 변하지 않는 상수처럼 사용하는 static final 변수는 대문자와 밑줄로 이름을 작성합니다.
다만 모든 final 지역 변수까지 반드시 대문자로 작성해야 하는 것은 아닙니다.
예제
public class Main {
public static void main(String[] args) {
final String name = "상구";
System.out.println(name);
// name = "민수";
// 컴파일 오류: final 변수에는 새로운 값을 다시 할당할 수 없음
}
}
// 실행 결과:
// 상구
name에는 처음에 "상구"가 저장됩니다.
그 뒤에는 "민수"처럼 새로운 문자열을 다시 할당할 수 없습니다.
final 참조 변수에서 주의할 점
객체를 저장하는 참조 변수에 final을 붙였다고 해서 객체 내부의 모든 값까지 변하지 않는 것은 아닙니다.
final이 막는 것은 참조 변수가 다른 객체를 가리키도록 바꾸는 것입니다.
class Person {
String name;
Person(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
final Person person = new Person("상구");
// 객체 내부의 필드는 변경 가능
person.name = "상구 수정";
System.out.println(person.name);
// person = new Person("민수");
// 컴파일 오류: person이 다른 객체를 가리키도록 변경할 수 없음
}
}
// 실행 결과:
// 상구 수정
person 변수는 처음 만들어진 Person 객체를 계속 가리켜야 합니다.
하지만 그 객체 안에 있는 name 필드가 final이 아니므로, person.name은 변경할 수 있습니다.
즉, final 참조 변수와 변경할 수 없는 객체는 서로 다른 개념입니다.
final 메서드
메서드에 final을 붙이면 자식 클래스가 해당 메서드를 오버라이딩할 수 없습니다.
오버라이딩은 부모 클래스에서 물려받은 메서드를 자식 클래스가 자신의 방식으로 다시 정의하는 것입니다.
기본 문법
class Parent {
public final void methodName() {
// 실행할 코드
}
}
예제
class Person {
private String name;
public Person(String name) {
this.name = name;
}
// 자식 클래스에서 변경할 수 없는 메서드
public final void printName() {
System.out.println("이름: " + name);
}
}
class Student extends Person {
public Student(String name) {
super(name);
}
/*
public void printName() {
System.out.println("학생 이름을 출력합니다.");
}
*/
// 위 메서드의 주석을 해제하면 컴파일 오류 발생
// 부모의 final 메서드는 오버라이딩할 수 없음
}
public class Main {
public static void main(String[] args) {
Student student = new Student("상구");
student.printName();
}
}
// 실행 결과:
// 이름: 상구
Student는 Person을 상속받았으므로 printName()을 사용할 수 있습니다.
하지만 printName()이 final로 선언되어 있기 때문에, Student 클래스에서 같은 메서드를 새롭게 정의할 수는 없습니다.
부모 클래스에서 정한 동작을 자식 클래스가 바꾸면 안 되는 상황에서 사용합니다.
final 클래스
클래스에 final을 붙이면 다른 클래스가 해당 클래스를 상속할 수 없습니다.
기본 문법
final class ClassName {
// 필드와 메서드
}
예제
final class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void introduce() {
System.out.println("저는 " + name + "입니다.");
}
}
/*
class Student extends Person {
public Student(String name) {
super(name);
}
}
*/
// 위 클래스의 주석을 해제하면 컴파일 오류 발생
// final 클래스는 상속할 수 없음
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
person.introduce();
}
}
// 실행 결과:
// 저는 상구입니다.
Person 클래스가 final로 선언되어 있으므로 다른 클래스가 extends Person으로 상속할 수 없습니다.
클래스의 동작을 상속으로 변경하거나 확장하는 것을 막고 싶을 때 사용할 수 있습니다.
Java의 String 클래스도 대표적인 final 클래스입니다.
final의 의미 정리
final은 사용하는 대상에 따라 의미를 구분해서 기억하는 것이 좋습니다.
| final 변수 | 다른 값을 다시 할당할 수 없음 |
| final 메서드 | 자식 클래스에서 오버라이딩할 수 없음 |
| final 클래스 | 다른 클래스가 상속할 수 없음 |
결국 final은 무언가를 무조건 “상수”로 만드는 문법이라기보다, 변경 가능한 범위를 제한하는 문법이라고 보면 이해하기 쉽습니다.
접근 제한자, this, final을 함께 사용해보기
final class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void introduce() {
System.out.println("저는 " + this.name + "입니다.");
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("상구");
person.introduce();
System.out.println("저장된 이름: " + person.getName());
}
}
// 실행 결과:
// 저는 상구입니다.
// 저장된 이름: 상구
먼저 Person 클래스가 final이므로 다른 클래스가 상속할 수 없습니다.
name 필드는 private이므로 클래스 밖에서 직접 접근할 수 없으며, 동시에 final이므로 생성된 뒤 다른 문자열로 다시 할당할 수 없습니다.
생성자에서는 this.name = name;을 사용해 전달받은 "상구"를 현재 객체의 name 필드에 저장합니다.
외부에서는 public으로 공개된 getName()과 introduce() 메서드만 사용할 수 있습니다.
마무리
오늘은 Java의 접근 제한자, this, final 키워드에 대해 정리해봤습니다.
제가 이해하기로는
접근 제한자(public, private)는 누가 사용할 수 있는지를 정한다.
this는 현재 어떤 객체를 가리키는지 나타낸다.
final은 어디까지 변경할 수 있는지를 제한한다.
로 이해가 됩니다.
또한 이러한 접근제한자와 키워드를 사용해 캡슐화를 시킬 수 있다는 점이 중요한 것 같다는 생각이 드네요.
공부화이팅!

'웹 개발 > JAVA' 카테고리의 다른 글
| [Java(8)] 생성자, static, string 클래스 (0) | 2026.06.24 |
|---|---|
| [Java(7)] 클래스, 메서드, 오버로딩 (0) | 2026.06.19 |
| [Java(5)] 흐름제어: break, continue, switch (0) | 2026.06.18 |
| [Java(6)] 배열, 한번에 여러 값을 다루는 가장 기본적인 방법 (0) | 2026.06.18 |
| [Java(4)] 반복문, for문부터 do-while문까지 흐름으로 이해하기 (0) | 2026.06.18 |
